Authorization, favorites and players have been moved to the API server

This commit is contained in:
2025-07-07 18:22:51 +03:00
parent ae85eda411
commit 4aad0c8d48
42 changed files with 500 additions and 1376 deletions

View File

@@ -1,7 +1,8 @@
'use client';
import { useState } from 'react';
import { signIn } from 'next-auth/react';
import { api } from '@/lib/api';
import { AxiosError } from 'axios';
import { useRouter } from 'next/navigation';
import styled from 'styled-components';
@@ -91,29 +92,27 @@ export default function AdminLoginClient() {
setIsLoading(true);
try {
const result = await signIn('credentials', {
const response = await api.post('/auth/login', {
email,
password,
isAdminLogin: 'true',
redirect: false,
});
if (result?.error) {
switch (result.error) {
case 'NOT_AN_ADMIN':
setError('У вас нет прав администратора');
break;
case 'EMAIL_NOT_VERIFIED':
setError('Пожалуйста, подтвердите свой email');
break;
default:
setError('Неверный email или пароль');
}
} else {
router.push('/admin');
const { token, user } = response.data;
if (user?.role !== 'admin') {
setError('У вас нет прав администратора.');
setIsLoading(false);
return;
}
} catch (error) {
setError('Произошла ошибка при входе');
localStorage.setItem('token', token);
localStorage.setItem('userName', user.name);
localStorage.setItem('userEmail', user.email);
router.push('/admin');
} catch (err) {
const axiosError = err as AxiosError<{ message: string }>;
setError(axiosError.response?.data?.message || 'Неверный email или пароль');
} finally {
setIsLoading(false);
}

View File

@@ -1,43 +0,0 @@
import { NextResponse } from 'next/server';
import { User } from '@/models';
import { connectDB } from '@/lib/db';
export async function POST(req: Request) {
try {
const { email, secret } = await req.json();
// Проверяем секретный ключ
const adminSecret = process.env.ADMIN_SECRET;
if (!adminSecret || secret !== adminSecret) {
return NextResponse.json(
{ error: 'Неверный секретный ключ' },
{ status: 403 }
);
}
await connectDB();
const user = await User.findOne({ email });
if (!user) {
return NextResponse.json(
{ error: 'Пользователь не найден' },
{ status: 404 }
);
}
// Назначаем пользователя администратором
user.isAdmin = true;
await user.save();
return NextResponse.json({
success: true,
message: 'Пользователь успешно назначен администратором'
});
} catch (error) {
console.error('Error creating admin:', error);
return NextResponse.json(
{ error: 'Произошла ошибка при назначении администратора' },
{ status: 500 }
);
}
}

View File

@@ -1,28 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { Movie } from '@/models';
import { connectDB } from '@/lib/db';
export async function GET() {
try {
await connectDB();
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const movies = await Movie.find().sort({ createdAt: -1 });
return NextResponse.json(movies);
} catch (error) {
console.error('Error fetching movies:', error);
return NextResponse.json(
{ error: 'Failed to fetch movies' },
{ status: 500 }
);
}
}

View File

@@ -1,46 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { Movie } from '@/models';
import { connectDB } from '@/lib/db';
export async function POST(request: Request) {
try {
await connectDB();
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const { movieId } = await request.json();
if (!movieId) {
return NextResponse.json(
{ error: 'Movie ID is required' },
{ status: 400 }
);
}
const movie = await Movie.findById(movieId);
if (!movie) {
return NextResponse.json(
{ error: 'Movie not found' },
{ status: 404 }
);
}
movie.isVisible = !movie.isVisible;
await movie.save();
return NextResponse.json({ success: true, movie });
} catch (error) {
console.error('Error toggling movie visibility:', error);
return NextResponse.json(
{ error: 'Failed to toggle movie visibility' },
{ status: 500 }
);
}
}

View File

@@ -1,42 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { User } from '@/models';
import { connectDB } from '@/lib/db';
import { sendVerificationEmail } from '@/lib/email';
import { generateVerificationToken } from '@/lib/utils';
export async function POST(req: Request) {
try {
const { email } = await req.json();
await connectDB();
const user = await User.findOne({ email });
if (!user || !user.isAdmin) {
return NextResponse.json(
{ error: 'Доступ запрещен' },
{ status: 403 }
);
}
if (!email) {
return NextResponse.json(
{ error: 'Email is required' },
{ status: 400 }
);
}
const token = generateVerificationToken();
await sendVerificationEmail(email, token);
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error sending verification email:', error);
return NextResponse.json(
{ error: 'Failed to send verification email' },
{ status: 500 }
);
}
}

View File

@@ -1,56 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { User } from '@/models';
import { connectDB } from '@/lib/db';
export async function POST(req: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json(
{ error: 'Доступ запрещен' },
{ status: 403 }
);
}
const { userId } = await req.json();
await connectDB();
const targetUser = await User.findById(userId);
if (!targetUser) {
return NextResponse.json(
{ error: 'Пользователь не найден' },
{ status: 404 }
);
}
// Проверяем, что это не последний администратор
if (targetUser.isAdmin) {
const adminCount = await User.countDocuments({ isAdmin: true });
if (adminCount <= 1) {
return NextResponse.json(
{ error: 'Нельзя отозвать права у последнего администратора' },
{ status: 400 }
);
}
}
// Переключаем статус администратора
targetUser.isAdmin = !targetUser.isAdmin;
await targetUser.save();
return NextResponse.json({
success: true,
isAdmin: targetUser.isAdmin,
});
} catch (error) {
console.error('Error toggling admin status:', error);
return NextResponse.json(
{ error: 'Произошла ошибка при изменении прав администратора' },
{ status: 500 }
);
}
}

View File

@@ -1,56 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { User } from '@/models';
import { connectDB } from '@/lib/db';
export async function POST(req: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json(
{ error: 'Доступ запрещен' },
{ status: 403 }
);
}
const { userId } = await req.json();
await connectDB();
const targetUser = await User.findById(userId);
if (!targetUser) {
return NextResponse.json(
{ error: 'Пользователь не найден' },
{ status: 404 }
);
}
// Проверяем, что это не последний администратор
if (targetUser.isAdmin) {
const adminCount = await User.countDocuments({ isAdmin: true });
if (adminCount <= 1) {
return NextResponse.json(
{ error: 'Нельзя отозвать права у последнего администратора' },
{ status: 400 }
);
}
}
// Переключаем статус администратора
targetUser.isAdmin = !targetUser.isAdmin;
await targetUser.save();
return NextResponse.json({
success: true,
isAdmin: targetUser.isAdmin,
});
} catch (error) {
console.error('Error toggling admin status:', error);
return NextResponse.json(
{ error: 'Произошла ошибка при изменении прав администратора' },
{ status: 500 }
);
}
}

View File

@@ -1,42 +0,0 @@
import { NextResponse } from 'next/server';
import { User } from '@/models';
import { connectDB } from '@/lib/db';
export async function POST(req: Request) {
try {
const { email, code } = await req.json();
await connectDB();
const user = await User.findOne({ email });
if (!user || !user.isAdmin) {
return NextResponse.json(
{ error: 'Доступ запрещен' },
{ status: 403 }
);
}
// Проверяем код
if (!user.adminVerificationCode ||
user.adminVerificationCode.code !== code ||
new Date() > new Date(user.adminVerificationCode.expiresAt)) {
return NextResponse.json(
{ error: 'Неверный или устаревший код подтверждения' },
{ status: 400 }
);
}
// Очищаем код после успешной проверки
user.adminVerificationCode = undefined;
await user.save();
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error verifying code:', error);
return NextResponse.json(
{ error: 'Произошла ошибка при проверке кода' },
{ status: 500 }
);
}
}

View File

@@ -1,38 +0,0 @@
import { NextResponse } from 'next/server';
export const revalidate = 0; // always fresh
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const imdbId = searchParams.get('imdb_id');
const tmdbId = searchParams.get('tmdb_id');
if (!imdbId && !tmdbId) {
return NextResponse.json({ error: 'imdb_id or tmdb_id query param is required' }, { status: 400 });
}
const token = process.env.ALLOHA_TOKEN;
if (!token) {
return NextResponse.json({ error: 'Server misconfiguration: ALLOHA_TOKEN missing' }, { status: 500 });
}
const idParam = imdbId ? `imdb=${encodeURIComponent(imdbId)}` : `tmdb=${encodeURIComponent(tmdbId!)}`;
const apiUrl = `https://api.alloha.tv/?token=${token}&${idParam}`;
const apiRes = await fetch(apiUrl, { next: { revalidate: 0 } });
if (!apiRes.ok) {
return NextResponse.json({ error: 'Failed to fetch from Alloha' }, { status: apiRes.status });
}
const json = await apiRes.json();
if (json.status !== 'success' || !json.data?.iframe) {
return NextResponse.json({ error: 'Video not found' }, { status: 404 });
}
return NextResponse.json({ iframe: json.data.iframe });
} catch (e) {
console.error('Alloha API route error:', e);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}

View File

@@ -1,98 +0,0 @@
import NextAuth, { DefaultSession } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { compare } from 'bcrypt';
import { connectToDatabase } from '@/lib/mongodb';
// Расширяем тип User в сессии
declare module 'next-auth' {
interface Session {
user: {
id: string;
name: string;
email: string;
verified: boolean;
isAdmin: boolean;
adminVerified?: boolean;
} & DefaultSession['user']
}
}
const handler = NextAuth({
providers: [
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
isAdminLogin: { label: 'isAdminLogin', type: 'boolean' }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error('Необходимо указать email и пароль');
}
const { db } = await connectToDatabase();
const user = await db.collection('users').findOne({ email: credentials.email });
if (!user) {
throw new Error('Пользователь не найден');
}
const isPasswordValid = await compare(credentials.password, user.password);
if (!isPasswordValid) {
throw new Error('Неверный пароль');
}
// Проверяем верификацию
if (!user.verified) {
throw new Error('EMAIL_NOT_VERIFIED');
}
// Если это попытка входа в админ-панель
if (credentials.isAdminLogin === 'true') {
// Проверяем, является ли пользователь админом
if (!user.isAdmin) {
throw new Error('NOT_AN_ADMIN');
}
}
return {
id: user._id.toString(),
email: user.email,
name: user.name,
verified: user.verified,
isAdmin: user.isAdmin,
adminVerified: credentials.isAdminLogin === 'true'
};
}
})
],
pages: {
signIn: '/login',
error: '/login'
},
callbacks: {
jwt({ token, user }) {
if (user) {
token.id = user.id;
token.verified = user.verified;
token.isAdmin = user.isAdmin;
token.adminVerified = user.adminVerified;
}
return token;
},
session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
session.user.verified = token.verified as boolean;
session.user.isAdmin = token.isAdmin as boolean;
session.user.adminVerified = token.adminVerified as boolean;
}
return session;
}
},
secret: process.env.NEXTAUTH_SECRET,
});
export { handler as GET, handler as POST };

View File

@@ -1,23 +0,0 @@
import { NextResponse } from 'next/server';
import { connectToDatabase } from '@/lib/mongodb';
export async function POST(req: Request) {
try {
const { email } = await req.json();
const { db } = await connectToDatabase();
const user = await db.collection('users').findOne({ email });
if (!user) {
return NextResponse.json({ error: 'Пользователь не найден' }, { status: 404 });
}
return NextResponse.json({ verified: user.verified ?? false });
} catch (error) {
console.error('Error checking verification status:', error);
return NextResponse.json(
{ error: 'Внутренняя ошибка сервера' },
{ status: 500 }
);
}
}

View File

@@ -1,58 +0,0 @@
import { NextResponse } from 'next/server';
import { hash } from 'bcryptjs';
import { connectToDatabase } from '@/lib/mongodb';
import { sendVerificationEmail } from '@/lib/mailer';
function generateVerificationCode(): string {
return Math.floor(100000 + Math.random() * 900000).toString();
}
export async function POST(req: Request) {
try {
const { email, password, name } = await req.json();
const { db } = await connectToDatabase();
// Проверяем, существует ли пользователь
const existingUser = await db.collection('users').findOne({ email });
if (existingUser) {
return NextResponse.json(
{ error: 'Email уже зарегистрирован' },
{ status: 400 }
);
}
// Хешируем пароль
const hashedPassword = await hash(password, 12);
// Генерируем код подтверждения
const verificationCode = generateVerificationCode();
const verificationExpires = new Date(Date.now() + 10 * 60 * 1000); // 10 минут
// Создаем пользователя
await db.collection('users').insertOne({
email,
password: hashedPassword,
name,
verified: false,
verificationCode,
verificationExpires,
isAdmin: false, // Явно указываем, что новый пользователь не админ
createdAt: new Date(),
});
// Отправляем код подтверждения
await sendVerificationEmail(email, verificationCode);
return NextResponse.json({
success: true,
email,
message: 'Пользователь успешно зарегистрирован',
});
} catch {
return NextResponse.json(
{ message: 'Ошибка при регистрации' },
{ status: 500 }
);
}
}

View File

@@ -1,48 +0,0 @@
import { NextResponse } from 'next/server';
import { connectToDatabase } from '@/lib/mongodb';
import { sendVerificationEmail } from '@/lib/mailer';
function generateVerificationCode(): string {
return Math.floor(100000 + Math.random() * 900000).toString();
}
export async function POST(req: Request) {
try {
const { email } = await req.json();
const { db } = await connectToDatabase();
const user = await db.collection('users').findOne({ email });
if (!user) {
return NextResponse.json(
{ error: 'Пользователь не найден' },
{ status: 404 }
);
}
// Генерируем новый код
const verificationCode = generateVerificationCode();
const verificationExpires = new Date(Date.now() + 10 * 60 * 1000); // 10 минут
// Обновляем код в базе
await db.collection('users').updateOne(
{ email },
{
$set: {
verificationCode,
verificationExpires,
},
}
);
// Отправляем новый код
await sendVerificationEmail(email, verificationCode);
return NextResponse.json({ success: true });
} catch {
return NextResponse.json(
{ message: 'Ошибка при отправке кода' },
{ status: 500 }
);
}
}

View File

@@ -1,51 +0,0 @@
import { NextResponse } from 'next/server';
import { connectToDatabase } from '@/lib/mongodb';
export async function POST(req: Request) {
try {
const { email, code } = await req.json();
const { db } = await connectToDatabase();
const user = await db.collection('users').findOne({ email });
if (!user) {
return NextResponse.json(
{ error: 'Пользователь не найден' },
{ status: 404 }
);
}
if (user.verificationCode !== code) {
return NextResponse.json(
{ error: 'Неверный код подтверждения' },
{ status: 400 }
);
}
if (user.verificationExpires < new Date()) {
return NextResponse.json(
{ error: 'Код подтверждения истек' },
{ status: 400 }
);
}
// Подтверждаем аккаунт
await db.collection('users').updateOne(
{ email },
{
$set: {
verified: true,
verificationCode: null,
verificationExpires: null,
},
}
);
return NextResponse.json({ success: true });
} catch {
return NextResponse.json(
{ message: 'Ошибка при подтверждении' },
{ status: 500 }
);
}
}

View File

@@ -1,41 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { connectToDatabase } from '@/lib/mongodb';
// DELETE /api/favorites/[mediaId] - удалить из избранного
export async function DELETE(
request: Request,
{ params }: { params: { mediaId: string } }
) {
try {
const session = await getServerSession();
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const mediaType = searchParams.get('mediaType');
const mediaId = params.mediaId;
if (!mediaType || !mediaId) {
return NextResponse.json({ error: 'Missing mediaType or mediaId' }, { status: 400 });
}
const { db } = await connectToDatabase();
const result = await db.collection('favorites').deleteOne({
userId: session.user.email,
mediaId,
mediaType
});
if (result.deletedCount === 0) {
return NextResponse.json({ error: 'Favorite not found' }, { status: 404 });
}
return NextResponse.json({ message: 'Removed from favorites' });
} catch (error) {
console.error('Error removing from favorites:', error);
return NextResponse.json({ error: 'Failed to remove from favorites' }, { status: 500 });
}
}

View File

@@ -1,32 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { connectToDatabase } from '@/lib/mongodb';
// GET /api/favorites/check/[mediaId] - проверить есть ли в избранном
export async function GET(
request: Request,
{ params }: { params: { mediaId: string } }
) {
try {
const session = await getServerSession();
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const mediaType = searchParams.get('mediaType');
const { db } = await connectToDatabase();
const favorite = await db.collection('favorites').findOne({
userId: session.user.email,
mediaId: params.mediaId,
mediaType
});
return NextResponse.json({ isFavorite: !!favorite });
} catch (error) {
console.error('Error checking favorite:', error);
return NextResponse.json({ error: 'Failed to check favorite status' }, { status: 500 });
}
}

View File

@@ -1,88 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { connectToDatabase, resetIndexes } from '@/lib/mongodb';
// Флаг для отслеживания инициализации
let isInitialized = false;
// GET /api/favorites - получить все избранные
export async function GET() {
try {
// Инициализируем индексы при первом запросе
if (!isInitialized) {
await resetIndexes();
isInitialized = true;
}
const session = await getServerSession();
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { db } = await connectToDatabase();
const favorites = await db.collection('favorites')
.find({ userId: session.user.email })
.sort({ createdAt: -1 })
.toArray();
return NextResponse.json(favorites);
} catch (error) {
console.error('Error getting favorites:', error);
return NextResponse.json({ error: 'Failed to get favorites' }, { status: 500 });
}
}
// POST /api/favorites - добавить в избранное
export async function POST(request: Request) {
try {
// Инициализируем индексы при первом запросе
if (!isInitialized) {
await resetIndexes();
isInitialized = true;
}
const session = await getServerSession();
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { mediaId, mediaType, title, posterPath } = await request.json();
if (!mediaId || !mediaType || !title) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
const { db } = await connectToDatabase();
const favorite = {
userId: session.user.email,
mediaId: mediaId.toString(), // Преобразуем в строку для консистентности
mediaType,
title,
posterPath,
createdAt: new Date()
};
// Используем updateOne с upsert вместо insertOne
const result = await db.collection('favorites').updateOne(
{
userId: session.user.email,
mediaId: favorite.mediaId,
mediaType
},
{ $set: favorite },
{ upsert: true }
);
// Если документ был обновлен (уже существовал)
if (result.matchedCount > 0) {
return NextResponse.json({ message: 'Already in favorites' }, { status: 200 });
}
// Если документ был создан (новый)
return NextResponse.json(favorite);
} catch (error) {
console.error('Error adding to favorites:', error);
return NextResponse.json({ error: 'Failed to add to favorites' }, { status: 500 });
}
}

View File

@@ -1,66 +0,0 @@
import { NextResponse } from 'next/server';
import { compare } from 'bcryptjs';
import { connectToDatabase } from '@/lib/mongodb';
import jwt from 'jsonwebtoken';
export async function POST(req: Request) {
try {
const { email, password } = await req.json();
const { db } = await connectToDatabase();
const user = await db.collection('users').findOne({ email });
if (!user) {
return NextResponse.json(
{ error: 'Пользователь не найден' },
{ status: 404 }
);
}
const isPasswordValid = await compare(password, user.password);
if (!isPasswordValid) {
return NextResponse.json(
{ error: 'Неверный пароль' },
{ status: 401 }
);
}
if (!user.verified) {
return NextResponse.json(
{ error: 'EMAIL_NOT_VERIFIED' },
{ status: 403 }
);
}
// Создаем JWT токен
const token = jwt.sign(
{
id: user._id.toString(),
email: user.email,
name: user.name,
verified: user.verified,
isAdmin: user.isAdmin
},
process.env.NEXTAUTH_SECRET!,
{ expiresIn: '30d' }
);
return NextResponse.json({
token,
user: {
id: user._id.toString(),
email: user.email,
name: user.name,
verified: user.verified,
isAdmin: user.isAdmin
}
});
} catch (error) {
console.error('Login error:', error);
return NextResponse.json(
{ error: 'Ошибка входа' },
{ status: 500 }
);
}
}

View File

@@ -1,61 +0,0 @@
import { NextResponse } from 'next/server';
import { hash } from 'bcryptjs';
import { connectToDatabase } from '@/lib/mongodb';
import { sendVerificationEmail } from '@/lib/mailer';
function generateVerificationCode(): string {
return Math.floor(100000 + Math.random() * 900000).toString();
}
export async function POST(req: Request) {
try {
const { email, password, name } = await req.json();
const { db } = await connectToDatabase();
// Проверяем, существует ли пользователь
const existingUser = await db.collection('users').findOne({ email });
if (existingUser) {
return NextResponse.json(
{ error: 'Email уже зарегистрирован' },
{ status: 400 }
);
}
// Хешируем пароль
const hashedPassword = await hash(password, 12);
// Генерируем код подтверждения
const verificationCode = generateVerificationCode();
const verificationExpires = new Date(Date.now() + 10 * 60 * 1000); // 10 минут
// Создаем пользователя
const result = await db.collection('users').insertOne({
email,
password: hashedPassword,
name,
verified: false,
verificationCode,
verificationExpires,
isAdmin: false,
createdAt: new Date(),
});
// Отправляем код подтверждения
await sendVerificationEmail(email, verificationCode);
return NextResponse.json({
id: result.insertedId.toString(),
email,
name,
verified: false,
isAdmin: false
});
} catch (error) {
console.error('Registration error:', error);
return NextResponse.json(
{ error: 'Ошибка регистрации' },
{ status: 500 }
);
}
}

View File

@@ -1,56 +0,0 @@
import { NextResponse } from 'next/server';
import { connectToDatabase } from '@/lib/mongodb';
import { sendVerificationEmail } from '@/lib/mailer';
function generateVerificationCode(): string {
return Math.floor(100000 + Math.random() * 900000).toString();
}
export async function POST(req: Request) {
try {
const { email } = await req.json();
const { db } = await connectToDatabase();
const user = await db.collection('users').findOne({ email });
if (!user) {
return NextResponse.json(
{ error: 'Пользователь не найден' },
{ status: 404 }
);
}
if (user.verified) {
return NextResponse.json(
{ error: 'Email уже подтвержден' },
{ status: 400 }
);
}
// Генерируем новый код
const verificationCode = generateVerificationCode();
const verificationExpires = new Date(Date.now() + 10 * 60 * 1000); // 10 минут
// Обновляем код в базе
await db.collection('users').updateOne(
{ _id: user._id },
{
$set: {
verificationCode,
verificationExpires
}
}
);
// Отправляем новый код
await sendVerificationEmail(email, verificationCode);
return NextResponse.json({ success: true });
} catch (error) {
console.error('Resend code error:', error);
return NextResponse.json(
{ error: 'Ошибка отправки кода' },
{ status: 500 }
);
}
}

View File

@@ -1,62 +0,0 @@
import { NextResponse } from 'next/server';
import { connectToDatabase } from '@/lib/mongodb';
import jwt from 'jsonwebtoken';
export async function POST(req: Request) {
try {
const { email, code } = await req.json();
const { db } = await connectToDatabase();
const user = await db.collection('users').findOne({
email,
verificationCode: code,
verificationExpires: { $gt: new Date() }
});
if (!user) {
return NextResponse.json(
{ error: 'Неверный код или код истек' },
{ status: 400 }
);
}
// Подтверждаем email
await db.collection('users').updateOne(
{ _id: user._id },
{
$set: { verified: true },
$unset: { verificationCode: "", verificationExpires: "" }
}
);
// Создаем JWT токен
const token = jwt.sign(
{
id: user._id.toString(),
email: user.email,
name: user.name,
verified: true,
isAdmin: user.isAdmin
},
process.env.NEXTAUTH_SECRET!,
{ expiresIn: '30d' }
);
return NextResponse.json({
token,
user: {
id: user._id.toString(),
email: user.email,
name: user.name,
verified: true,
isAdmin: user.isAdmin
}
});
} catch (error) {
console.error('Verification error:', error);
return NextResponse.json(
{ error: 'Ошибка верификации' },
{ status: 500 }
);
}
}

View File

@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import styled from 'styled-components';
import Link from 'next/link';
import Image from 'next/image';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { favoritesAPI } from '@/lib/favoritesApi';
import { getImageUrl } from '@/lib/neoApi';
@@ -87,14 +87,15 @@ interface Favorite {
}
export default function FavoritesPage() {
const { data: session } = useSession();
const [favorites, setFavorites] = useState<Favorite[]>([]);
const [loading, setLoading] = useState(true);
const router = useRouter();
useEffect(() => {
const fetchFavorites = async () => {
if (!session?.user) {
setLoading(false);
const token = localStorage.getItem('token');
if (!token) {
router.push('/login');
return;
}
@@ -102,14 +103,19 @@ export default function FavoritesPage() {
const response = await favoritesAPI.getFavorites();
setFavorites(response.data);
} catch (error) {
console.error('Error fetching favorites:', error);
console.error('Failed to fetch favorites:', error);
// If token is invalid, clear it and redirect to login
localStorage.removeItem('token');
localStorage.removeItem('userName');
localStorage.removeItem('userEmail');
router.push('/login');
} finally {
setLoading(false);
}
};
fetchFavorites();
}, [session?.user]);
}, [router]);
if (loading) {
return (
@@ -120,16 +126,7 @@ export default function FavoritesPage() {
);
}
if (!session?.user) {
return (
<Container>
<Title>Избранное</Title>
<EmptyState>
Для доступа к избранному необходимо авторизоваться
</EmptyState>
</Container>
);
}
if (favorites.length === 0) {
return (

View File

@@ -1,8 +1,8 @@
'use client';
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { signIn } from 'next-auth/react';
import { useAuth } from '../../hooks/useAuth';
import { useRouter } from 'next/navigation';
const Container = styled.div`
@@ -173,6 +173,14 @@ export default function LoginClient() {
const [name, setName] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const { login, register } = useAuth();
// Redirect authenticated users away from /login
useEffect(() => {
if (typeof window !== 'undefined' && localStorage.getItem('token')) {
router.replace('/');
}
}, [router]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -180,38 +188,11 @@ export default function LoginClient() {
try {
if (isLogin) {
const result = await signIn('credentials', {
redirect: false,
email,
password,
});
if (result?.error) {
if (result.error === 'EMAIL_NOT_VERIFIED') {
router.push(`/verify?email=${encodeURIComponent(email)}`);
return;
}
throw new Error(result.error);
}
router.push('/');
await login(email, password);
} else {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name }),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Ошибка при регистрации');
}
const data = await response.json();
await register(email, password, name);
// Сохраняем пароль для автовхода после верификации
localStorage.setItem('password', password);
router.push(`/verify?email=${encodeURIComponent(email)}`);
}
} catch (err) {

View File

@@ -0,0 +1,129 @@
'use client';
import dynamic from 'next/dynamic';
import styled from 'styled-components';
const LoginClient = dynamic(() => import('./LoginClient'), {
ssr: false
});
export default function LoginPage() {
return (
<Container>
<GlowingBackground>
<Glow1 />
<Glow2 />
<Glow3 />
</GlowingBackground>
<Content>
<Logo>
<span>Neo</span> Movies
</Logo>
<GlassCard>
<LoginClient />
</GlassCard>
</Content>
</Container>
);
}
const Container = styled.div`
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
background-color: #0a0a0a;
overflow: hidden;
`;
const Content = styled.main`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
max-width: 1200px;
padding: 2rem;
position: relative;
z-index: 1;
`;
const Logo = styled.h1`
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 2rem;
color: white;
text-align: center;
span {
color: #2196f3;
}
`;
const GlassCard = styled.div`
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 3rem;
border-radius: 24px;
width: 100%;
max-width: 500px;
box-shadow:
0 8px 32px 0 rgba(0, 0, 0, 0.3),
inset 0 0 0 1px rgba(255, 255, 255, 0.05);
margin: 0 auto;
`;
const GlowingBackground = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
z-index: 0;
`;
const Glow = styled.div`
position: absolute;
border-radius: 50%;
filter: blur(100px);
opacity: 0.3;
animation: float 20s infinite ease-in-out;
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-30px, 30px); }
}
`;
const Glow1 = styled(Glow)`
background: #2196f3;
width: 600px;
height: 600px;
top: -200px;
left: -200px;
animation-delay: 0s;
`;
const Glow2 = styled(Glow)`
background: #9c27b0;
width: 500px;
height: 500px;
bottom: -150px;
right: -150px;
animation-delay: -5s;
`;
const Glow3 = styled(Glow)`
background: #00bcd4;
width: 400px;
height: 400px;
bottom: 100px;
left: 30%;
animation-delay: -10s;
`;

View File

@@ -9,14 +9,13 @@ interface PageProps {
}
// Генерация метаданных для страницы
export async function generateMetadata(
props: { params: { id: string }}
): Promise<Metadata> {
export async function generateMetadata(props: Promise<PageProps>): Promise<Metadata> {
const { params } = await props;
// В Next.js 14, нужно сначала получить данные фильма,
// а затем использовать их для метаданных
try {
// Получаем id для использования в запросе
const movieId = props.params.id;
const movieId = params.id;
// Запрашиваем данные фильма
const { data: movie } = await moviesAPI.getMovie(movieId);

View File

@@ -1,10 +1,10 @@
'use client';
import { useSession } from 'next-auth/react';
import { useAuth } from '@/hooks/useAuth';
import styled from 'styled-components';
import GlassCard from '@/components/GlassCard';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
const Container = styled.div`
min-height: 100vh;
@@ -70,16 +70,28 @@ const SignOutButton = styled.button`
`;
export default function ProfilePage() {
const { data: session, status } = useSession();
const { logout } = useAuth();
const router = useRouter();
const [userName, setUserName] = useState<string | null>(null);
const [userEmail, setUserEmail] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (status === 'unauthenticated') {
const token = localStorage.getItem('token');
if (!token) {
router.push('/login');
} else {
setUserName(localStorage.getItem('userName'));
setUserEmail(localStorage.getItem('userEmail'));
setLoading(false);
}
}, [status, router]);
}, [router]);
if (status === 'loading') {
const handleSignOut = () => {
logout();
};
if (loading) {
return (
<Container>
<Content>
@@ -91,7 +103,7 @@ export default function ProfilePage() {
);
}
if (!session) {
if (!userName) {
return null;
}
@@ -101,14 +113,12 @@ export default function ProfilePage() {
<GlassCard>
<ProfileHeader>
<Avatar>
{session.user?.name?.split(' ').map(n => n[0]).join('').toUpperCase() || ''}
{userName?.split(' ').map(n => n[0]).join('').toUpperCase() || ''}
</Avatar>
<Name>{session.user?.name}</Name>
<Email>{session.user?.email}</Email>
<Name>{userName}</Name>
<Email>{userEmail}</Email>
<SignOutButton onClick={handleSignOut}>Выйти</SignOutButton>
</ProfileHeader>
<SignOutButton onClick={() => router.push('/settings')}>
Настройки
</SignOutButton>
</GlassCard>
</Content>
</Container>

View File

@@ -1,32 +0,0 @@
'use client';
import { ThemeProvider } from 'styled-components';
import { SessionProvider } from 'next-auth/react';
const theme = {
colors: {
primary: '#2196f3',
background: '#121212',
surface: '#1e1e1e',
text: '#ffffff',
textSecondary: 'rgba(255, 255, 255, 0.7)',
error: '#f44336',
success: '#4caf50',
},
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
},
};
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider theme={theme}>
<SessionProvider refetchInterval={0} refetchOnWindowFocus={false}>
{children}
</SessionProvider>
</ThemeProvider>
);
}

View File

@@ -3,7 +3,8 @@
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { useRouter, useSearchParams } from 'next/navigation';
import { signIn } from 'next-auth/react';
import { useAuth } from '../../hooks/useAuth';
import { authAPI } from '@/lib/authApi';
const Container = styled.div`
width: 100%;
@@ -40,7 +41,7 @@ const CodeInput = styled.input`
&:focus {
outline: none;
border-color: ${({ theme }) => theme.colors.primary};
border-color: #2196f3;
box-shadow: 0 0 0 4px rgba(33, 150, 243, 0.1);
}
@@ -82,7 +83,7 @@ const VerifyButton = styled.button`
const ResendButton = styled.button`
background: none;
border: none;
color: ${({ theme }) => theme.colors.primary};
color: #2196f3;
font-size: 0.875rem;
cursor: pointer;
padding: 0.5rem;
@@ -110,6 +111,7 @@ export function VerificationClient({ email }: { email: string }) {
const [isLoading, setIsLoading] = useState(false);
const [countdown, setCountdown] = useState(0);
const router = useRouter();
const { verifyCode, login } = useAuth();
const searchParams = useSearchParams();
useEffect(() => {
@@ -134,32 +136,7 @@ export function VerificationClient({ email }: { email: string }) {
setError('');
try {
const response = await fetch('/api/auth/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, code }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Ошибка верификации');
}
// Выполняем вход после успешной верификации
const result = await signIn('credentials', {
redirect: false,
email,
password: localStorage.getItem('password'),
});
if (result?.error) {
throw new Error(result.error);
}
router.push('/');
await verifyCode(code);
} catch (err) {
setError(err instanceof Error ? err.message : 'Произошла ошибка');
} finally {
@@ -169,18 +146,7 @@ export function VerificationClient({ email }: { email: string }) {
const handleResend = async () => {
try {
const response = await fetch('/api/auth/resend-code', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
if (!response.ok) {
throw new Error('Не удалось отправить код');
}
await authAPI.resendCode(email);
setCountdown(60);
} catch (err) {
setError('Не удалось отправить код');