This commit is contained in:
Adrián Borrageiros Mourelos 2025-05-13 11:25:43 +02:00
parent 03eae11b3b
commit 3a99ab39c6
23 changed files with 11689 additions and 1430 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
MONGODB_URI=mongodb+srv://adrianborrageirosmourelos:FOcz19ZnmULvFZeY@cluster0.gf3secu.mongodb.net/draw_dev?retryWrites=true&w=majority&appName=Cluster0

2
.gitignore vendored
View File

@ -31,7 +31,7 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
.env
# vercel
.vercel

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
20

6
next.config.js Normal file
View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

10277
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
@ -21,11 +21,15 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "10",
"eslint": "^9",
"eslint-config-next": "15.3.2",
"postcss": "8",
"tailwindcss": "3",
"typescript": "^5"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

5
postcss.config.mjs Normal file
View File

@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

View File

@ -1,12 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { login } from '@/controllers/authController';
import dbConnect from '@/lib/db/connection';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
await dbConnect();
return login(req, res);
}

View File

@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from "next/server";
import { loginUser } from "@/controllers/authController";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Llamar a la función loginUser del controlador
const { token, user, error, status } = await loginUser(body);
// Si hay un error, devolverlo con el status correspondiente
if (error) {
return NextResponse.json({ error }, { status: status || 500 });
}
// Devolver la respuesta con el token y el usuario
return NextResponse.json({ token, user }, { status: 200 });
} catch (error) {
console.error("Error en el login:", error);
return NextResponse.json(
{ error: "Error interno del servidor" },
{ status: 500 }
);
}
}

View File

@ -1,12 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { register } from '@/controllers/authController';
import dbConnect from '@/lib/db/connection';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
await dbConnect();
return register(req, res);
}

View File

@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from "next/server";
import { register } from "@/controllers/authController";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Llamar a la función register del controlador
const { token, user, error, status } = await register(body);
// Si hay un error, devolverlo con el status correspondiente
if (error) {
return NextResponse.json({ error }, { status: status || 500 });
}
// Devolver la respuesta con el token y el usuario
return NextResponse.json({ token, user }, { status: 201 });
} catch (error) {
console.error("Error en el registro:", error);
return NextResponse.json(
{ error: "Error interno del servidor" },
{ status: 500 }
);
}
}

161
src/app/auth/login/page.tsx Normal file
View File

@ -0,0 +1,161 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
export default function Login() {
const router = useRouter();
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
setLoading(true);
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: formData.email,
password: formData.password,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Error al iniciar sesión');
}
localStorage.setItem('token', data.token);
router.push('/dashboard');
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('Ha ocurrido un error desconocido');
}
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-6">
<div className="max-w-md w-full mx-auto">
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="px-8 pt-8 pb-6 border-b border-gray-200">
<h2 className="text-2xl font-normal text-gray-800 mb-2">Iniciar sesión</h2>
<p className="text-sm text-gray-500">
Accede a tu cuenta para comenzar a dibujar
</p>
</div>
<div className="px-8 py-6">
{error && (
<div className="mb-4 px-4 py-3 bg-red-50 text-sm text-red-600 rounded-md">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Correo electrónico
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={formData.email}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
placeholder="tu@ejemplo.com"
/>
</div>
<div>
<div className="flex items-center justify-between">
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Contraseña
</label>
<div className="text-xs">
<a href="#" className="text-gray-500 hover:text-gray-700">
¿Olvidaste tu contraseña?
</a>
</div>
</div>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
value={formData.password}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
placeholder="••••••••"
/>
</div>
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-0 border-gray-300 rounded"
/>
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-600">
Recordarme
</label>
</div>
<div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Iniciando sesión...
</>
) : 'Iniciar sesión'}
</button>
</div>
</form>
</div>
<div className="px-8 py-4 bg-gray-50 border-t border-gray-200">
<p className="text-sm text-center text-gray-600">
¿No tienes una cuenta?{' '}
<Link href="/auth/register" className="text-blue-600 hover:text-blue-500 font-medium">
Regístrate
</Link>
</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,190 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
export default function Register() {
const router = useRouter();
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: ''
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (formData.password !== formData.confirmPassword) {
setError('Las contraseñas no coinciden');
return;
}
if (formData.password.length < 6) {
setError('La contraseña debe tener al menos 6 caracteres');
return;
}
try {
setLoading(true);
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: formData.username,
email: formData.email,
password: formData.password,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Error al registrar usuario');
}
localStorage.setItem('token', data.token);
router.push('/dashboard');
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('Ha ocurrido un error desconocido');
}
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-6">
<div className="max-w-md w-full mx-auto">
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="px-8 pt-8 pb-6 border-b border-gray-200">
<h2 className="text-2xl font-normal text-gray-800 mb-2">Crear cuenta</h2>
<p className="text-sm text-gray-500">
Regístrate para comenzar a dibujar
</p>
</div>
<div className="px-8 py-6">
{error && (
<div className="mb-4 px-4 py-3 bg-red-50 text-sm text-red-600 rounded-md">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
Nombre de usuario
</label>
<input
id="username"
name="username"
type="text"
autoComplete="username"
required
value={formData.username}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
placeholder="usuario"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Correo electrónico
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={formData.email}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
placeholder="tu@ejemplo.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Contraseña
</label>
<input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
value={formData.password}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
placeholder="••••••••"
/>
<p className="mt-1 text-xs text-gray-500">Mínimo 6 caracteres</p>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
Confirmar contraseña
</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
autoComplete="new-password"
required
value={formData.confirmPassword}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
placeholder="••••••••"
/>
</div>
<div className="pt-2">
<button
type="submit"
disabled={loading}
className="w-full flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Creando cuenta...
</>
) : 'Crear cuenta'}
</button>
</div>
</form>
</div>
<div className="px-8 py-4 bg-gray-50 border-t border-gray-200">
<p className="text-sm text-center text-gray-600">
¿Ya tienes una cuenta?{' '}
<Link href="/auth/login" className="text-blue-600 hover:text-blue-500 font-medium">
Inicia sesión
</Link>
</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;

View File

@ -1,24 +0,0 @@
"use client"
import LoginForm from '@/components/LoginForm';
import Link from 'next/link';
export default function Login() {
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>
<LoginForm />
<div className="text-center">
<Link href="/register" className="text-blue-500 hover:text-blue-600">
Dont have an account? Register
</Link>
</div>
</div>
</div>
);
}

View File

@ -1,58 +1,69 @@
'use client';
import { useState, useEffect } from 'react';
import styles from "./page.module.css";
interface Drawing {
_id: string;
title: string;
content: string;
creator: string;
createdAt: string;
}
import Link from 'next/link';
export default function Home() {
const [drawings, setDrawings] = useState<Drawing[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchDrawings = async () => {
try {
const response = await fetch('/api/drawings');
const data = await response.json();
if (data.success) {
setDrawings(data.data);
}
} catch {
console.error('Error fetching drawings');
} finally {
setLoading(false);
}
};
fetchDrawings();
}, []);
return (
<div className={styles.page}>
<h1>Mis Dibujos</h1>
<div className="min-h-screen bg-gray-100 flex flex-col">
<header className="py-6 bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between items-center">
<div className="font-medium text-lg text-gray-900">DrawApp</div>
<nav>
<Link
href="/auth/login"
className="text-sm text-gray-700 hover:text-gray-900 mr-4"
>
Iniciar sesión
</Link>
<Link
href="/auth/register"
className="text-sm bg-blue-600 text-white py-2 px-3 rounded-md hover:bg-blue-700"
>
Registrarse
</Link>
</nav>
</div>
</header>
{loading ? (
<p>Cargando dibujos...</p>
) : drawings.length > 0 ? (
<ul>
{drawings.map((drawing) => (
<li key={drawing._id}>
<h2>{drawing.title}</h2>
<p>Creado por: {drawing.creator}</p>
<p>Fecha: {new Date(drawing.createdAt).toLocaleDateString()}</p>
</li>
))}
</ul>
) : (
<p>No hay dibujos disponibles</p>
)}
<main className="flex-grow flex">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 flex flex-col lg:flex-row items-center">
<div className="lg:w-1/2 lg:pr-12 mb-10 lg:mb-0">
<h1 className="text-4xl font-bold text-gray-900 mb-6">
Dibuja lo que imagines
</h1>
<p className="text-xl text-gray-600 mb-8">
Una aplicación simple y poderosa para expresar tu creatividad mediante dibujos digitales.
</p>
<div className="flex flex-col sm:flex-row space-y-3 sm:space-y-0 sm:space-x-4">
<Link
href="/auth/register"
className="inline-flex justify-center items-center px-6 py-3 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none"
>
Comenzar gratis
</Link>
<Link
href="/about"
className="inline-flex justify-center items-center px-6 py-3 border border-gray-300 rounded-md shadow-sm text-base font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none"
>
Conocer más
</Link>
</div>
</div>
<div className="lg:w-1/2 flex justify-center">
<div className="w-full max-w-md h-96 bg-white rounded-lg shadow-md flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1} stroke="currentColor" className="w-24 h-24 text-gray-300">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
</svg>
</div>
</div>
</div>
</main>
<footer className="bg-white border-t border-gray-200 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<p className="text-sm text-gray-500 text-center">
© 2023 DrawApp. Todos los derechos reservados.
</p>
</div>
</footer>
</div>
);
}

View File

@ -1,24 +0,0 @@
"use client"
import RegisterForm from '@/components/RegisterForm';
import Link from 'next/link';
export default function Register() {
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>
<RegisterForm />
<div className="text-center">
<Link href="/login" className="text-blue-500 hover:text-blue-600">
Already have an account? Login
</Link>
</div>
</div>
</div>
);
}

View File

@ -1,74 +0,0 @@
"use client"
import { useState } from 'react';
import { LoginData } from '../types/auth';
export default function LoginForm() {
const [formData, setFormData] = useState<LoginData>({
email: '',
password: '',
});
const [error, setError] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Login failed');
}
localStorage.setItem('token', data.token);
window.location.href = '/dashboard';
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed');
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
className="w-full p-2 border rounded"
required
/>
</div>
<div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
className="w-full p-2 border rounded"
required
/>
</div>
{error && <p className="text-red-500">{error}</p>}
<button
type="submit"
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
>
Login
</button>
</form>
);
}

View File

@ -1,87 +0,0 @@
"use client"
import { useState } from 'react';
import { RegisterData } from '../types/auth';
export default function RegisterForm() {
const [formData, setFormData] = useState<RegisterData>({
username: '',
email: '',
password: '',
});
const [error, setError] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Registration failed');
}
localStorage.setItem('token', data.token);
window.location.href = '/dashboard';
} catch (err) {
setError(err instanceof Error ? err.message : 'Registration failed');
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
placeholder="Username"
className="w-full p-2 border rounded"
required
/>
</div>
<div>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
className="w-full p-2 border rounded"
required
/>
</div>
<div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
className="w-full p-2 border rounded"
required
minLength={6}
/>
</div>
{error && <p className="text-red-500">{error}</p>}
<button
type="submit"
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
>
Register
</button>
</form>
);
}

View File

@ -1,17 +1,23 @@
import { NextApiRequest, NextApiResponse } from 'next';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import User from '../models/User';
import { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import User from "../models/User";
import dbConnect from "@/lib/db/connection";
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
export async function register(req: NextApiRequest, res: NextApiResponse) {
export async function register(data: {
username: string;
email: string;
password: string;
}) {
try {
const { username, email, password } = req.body;
await dbConnect();
const { username, email, password } = data;
const existingUser = await User.findOne({ $or: [{ email }, { username }] });
if (existingUser) {
return res.status(400).json({ error: 'User already exists' });
return { error: "User already exists", status: 400 };
}
const hashedPassword = await bcrypt.hash(password, 10);
@ -22,44 +28,42 @@ export async function register(req: NextApiRequest, res: NextApiResponse) {
password: hashedPassword,
});
const token = jwt.sign(
{ userId: user._id },
JWT_SECRET,
{ expiresIn: '7d' }
);
const token = jwt.sign({ userId: user._id }, JWT_SECRET, {
expiresIn: "7d",
});
return res.status(201).json({
return {
token,
user: {
id: user._id,
username: user.username,
email: user.email,
},
});
};
} catch (error) {
return res.status(500).json({ error: 'Internal server error' });
console.error("Error en el registro:", error);
return { error: "Internal server error", status: 500 };
}
}
export async function login(req: NextApiRequest, res: NextApiResponse) {
try {
await dbConnect();
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
return res.status(401).json({ error: "Invalid credentials" });
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
return res.status(401).json({ error: "Invalid credentials" });
}
const token = jwt.sign(
{ userId: user._id },
JWT_SECRET,
{ expiresIn: '7d' }
);
const token = jwt.sign({ userId: user._id }, JWT_SECRET, {
expiresIn: "7d",
});
return res.status(200).json({
token,
@ -69,7 +73,40 @@ export async function login(req: NextApiRequest, res: NextApiResponse) {
email: user.email,
},
});
} catch (error) {
return res.status(500).json({ error: 'Internal server error' });
} catch {
return res.status(500).json({ error: "Internal server error" });
}
}
}
export async function loginUser(data: { email: string; password: string }) {
try {
await dbConnect();
const { email, password } = data;
const user = await User.findOne({ email });
if (!user) {
return { error: "Invalid credentials", status: 401 };
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return { error: "Invalid credentials", status: 401 };
}
const token = jwt.sign({ userId: user._id }, JWT_SECRET, {
expiresIn: "7d",
});
return {
token,
user: {
id: user._id,
username: user.username,
email: user.email,
},
};
} catch (error) {
console.error("Error en el login:", error);
return { error: "Internal server error", status: 500 };
}
}

12
tailwind.config.js Normal file
View File

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
}

1963
yarn.lock

File diff suppressed because it is too large Load Diff