auth in backend and frontend

This commit is contained in:
echo committed 2025-03-24 02:32:57 +01:00
1 parent 39d2698f28
commit 9857acd4c1
65 files changed
+1632 -3923

No files matched your search

+2
View File
@@ -9,6 +9,8 @@
"lint": "next lint"
},
"dependencies": {
"@mono/auth-ui": "file:../../packages/auth-ui",
"@mono/auth-core": "file:../../packages/auth-core",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.2.3"
+107
View File
@@ -0,0 +1,107 @@
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoginSchema } from '@mono/auth-core';
import { useAuth } from '@/hooks/useAuth';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
export default function LoginPage() {
const [error, setError] = useState('');
const router = useRouter();
const { login } = useAuth();
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(LoginSchema),
});
const onSubmit = async (data: any) => {
try {
await login(data);
router.push('/dashboard');
} catch (err) {
setError('Invalid email or password');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit(onSubmit)}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
{...register('email')}
id="email"
type="email"
autoComplete="email"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Email address"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">
{errors.email.message as string}
</p>
)}
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
{...register('password')}
id="password"
type="password"
autoComplete="current-password"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600">
{errors.password.message as string}
</p>
)}
</div>
</div>
{error && (
<div className="text-sm text-red-600 text-center">{error}</div>
)}
<div>
<button
type="submit"
disabled={isSubmitting}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{isSubmitting ? 'Signing in...' : 'Sign in'}
</button>
</div>
</form>
<div className="text-sm text-center">
<Link
href="/auth/register"
className="font-medium text-indigo-600 hover:text-indigo-500"
>
Don't have an account? Register
</Link>
</div>
</div>
</div>
);
}
@@ -0,0 +1,120 @@
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { RegisterSchema } from '@mono/auth-core';
import { useAuth } from '@/hooks/useAuth';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
export default function RegisterPage() {
const [error, setError] = useState('');
const router = useRouter();
const { register: registerUser } = useAuth();
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(RegisterSchema),
});
const onSubmit = async (data: any) => {
try {
await registerUser(data);
router.push('/dashboard');
} catch (err) {
setError('Registration failed. Please try again.');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Create your account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit(onSubmit)}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
{...register('email')}
id="email"
type="email"
autoComplete="email"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Email address"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">
{errors.email.message as string}
</p>
)}
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
{...register('password')}
id="password"
type="password"
autoComplete="new-password"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600">
{errors.password.message as string}
</p>
)}
</div>
<div>
<label htmlFor="name" className="sr-only">
Name
</label>
<input
{...register('name')}
id="name"
type="text"
autoComplete="name"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Full name (optional)"
/>
</div>
</div>
{error && (
<div className="text-sm text-red-600 text-center">{error}</div>
)}
<div>
<button
type="submit"
disabled={isSubmitting}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{isSubmitting ? 'Registering...' : 'Register'}
</button>
</div>
</form>
<div className="text-sm text-center">
<Link
href="/auth/login"
className="font-medium text-indigo-600 hover:text-indigo-500"
>
Already have an account? Sign in
</Link>
</div>
</div>
</div>
);
}
@@ -0,0 +1,57 @@
'use client';
import { useAuth } from '@/hooks/useAuth';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
export default function DashboardPage() {
const { user, isAuthenticated, logout } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isAuthenticated) {
router.push('/auth/login');
}
}, [isAuthenticated, router]);
if (!user) {
return null;
}
return (
<div className="min-h-screen bg-gray-100">
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-semibold">Dashboard</h1>
</div>
<div className="flex items-center">
<span className="text-gray-700 mr-4">Welcome, {user.name || user.email}</span>
<button
onClick={() => {
logout();
router.push('/auth/login');
}}
className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md text-sm font-medium"
>
Logout
</button>
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<div className="border-4 border-dashed border-gray-200 rounded-lg h-96 p-4">
<h2 className="text-2xl font-bold mb-4">Welcome to your Dashboard</h2>
<p className="text-gray-600">
This is a protected page. You can only see this if you're logged in.
</p>
</div>
</div>
</main>
</div>
);
}
+9 -21
View File
@@ -1,34 +1,22 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: 'Next.js App with Auth',
description: 'A Next.js app with authentication',
};
export default function RootLayout({
children,
}: Readonly<{
}: {
children: React.ReactNode;
}>) {
}) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
<body className={inter.className}>{children}</body>
</html>
);
}
+7
View File
@@ -0,0 +1,7 @@
import { createAuthHook } from '@mono/auth-ui';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
export const useAuth = createAuthHook({
apiUrl: API_URL,
});
+3 -2
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2017",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
@@ -19,7 +19,8 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@hooks/*": ["./src/hooks/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],