From 020218275f5e095e32efc35fa0f42596f87c213a Mon Sep 17 00:00:00 2001 From: ren Date: Thu, 29 Jan 2026 14:57:19 +0100 Subject: [PATCH] Feat: Add complete auth system (Login, Register, OTP/2FA via Telegram, Session management) --- app/api/auth/login/route.ts | 54 ++++++++ app/api/auth/register/route.ts | 41 ++++++ app/api/auth/send/route.ts | 25 ---- app/api/auth/verify-otp/route.ts | 34 +++++ app/api/auth/verify/route.ts | 24 ---- app/login/page.tsx | 220 +++++++++++++++++++++---------- app/register/page.tsx | 135 +++++++++++++++++++ data/db.json | 19 +-- lib/auth.ts | 84 ++++++++++++ lib/otp.ts | 65 ++++++--- lib/types.ts | 9 ++ middleware.ts | 83 +++++++++--- package-lock.json | 27 ++++ package.json | 3 + 14 files changed, 645 insertions(+), 178 deletions(-) create mode 100644 app/api/auth/login/route.ts create mode 100644 app/api/auth/register/route.ts delete mode 100644 app/api/auth/send/route.ts create mode 100644 app/api/auth/verify-otp/route.ts delete mode 100644 app/api/auth/verify/route.ts create mode 100644 app/register/page.tsx create mode 100644 lib/auth.ts diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts new file mode 100644 index 0000000..8b0b94b --- /dev/null +++ b/app/api/auth/login/route.ts @@ -0,0 +1,54 @@ +import { NextResponse } from 'next/server'; +import { findUser, verifyPassword, createSession } from '@/lib/auth'; +import { generateOTP } from '@/lib/otp'; +import TelegramBot from 'node-telegram-bot-api'; + +const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; + +async function sendTelegramOTP(chatId: string, otp: string) { + if (!BOT_TOKEN) return false; + try { + const bot = new TelegramBot(BOT_TOKEN, { polling: false }); + await bot.sendMessage(chatId, `🔐 *Código de Acceso Finanzas*\n\nTu código es: \`${otp}\`\n\nSi no intentaste ingresar, ignora este mensaje.`, { parse_mode: 'Markdown' }); + return true; + } catch (e) { + console.error('Telegram send error:', e); + return false; + } +} + +export async function POST(req: Request) { + try { + const { username, password } = await req.json(); + const ip = req.headers.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown'; + + const user = findUser(username); + + if (!user || !(await verifyPassword(password, user.passwordHash))) { + return NextResponse.json({ error: 'Credenciales inválidas' }, { status: 401 }); + } + + // Check IP + const isKnownIp = user.knownIps.includes(ip); + + if (isKnownIp) { + // Login success directly + await createSession(user); + return NextResponse.json({ success: true, requireOtp: false }); + } else { + // Require OTP + const otp = generateOTP(user.username); + + const sent = await sendTelegramOTP(user.chatId, otp); + + if (!sent) { + return NextResponse.json({ error: 'No se pudo enviar el código OTP a Telegram' }, { status: 500 }); + } + + return NextResponse.json({ success: true, requireOtp: true }); + } + } catch (error) { + console.error('Login error:', error); + return NextResponse.json({ error: 'Error interno' }, { status: 500 }); + } +} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts new file mode 100644 index 0000000..ffb5c4f --- /dev/null +++ b/app/api/auth/register/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from 'next/server'; +import { saveUser, findUser, hashPassword, createSession } from '@/lib/auth'; +import { randomUUID } from 'crypto'; + +export async function POST(req: Request) { + try { + const { username, password, chatId } = await req.json(); + + if (!username || !password || !chatId) { + return NextResponse.json({ error: 'Faltan datos requeridos' }, { status: 400 }); + } + + if (findUser(username)) { + return NextResponse.json({ error: 'El usuario ya existe' }, { status: 409 }); + } + + // Hash password + const passwordHash = await hashPassword(password); + + // Get IP + const ip = req.headers.get('x-forwarded-for') || '127.0.0.1'; + + const newUser = { + id: randomUUID(), + username, + passwordHash, + chatId, + knownIps: [ip] // Register current IP as known initially + }; + + saveUser(newUser); + + // Auto login after register + await createSession(newUser); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Register error:', error); + return NextResponse.json({ error: 'Error interno del servidor' }, { status: 500 }); + } +} diff --git a/app/api/auth/send/route.ts b/app/api/auth/send/route.ts deleted file mode 100644 index ac024b2..0000000 --- a/app/api/auth/send/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NextResponse } from 'next/server'; -import TelegramBot from 'node-telegram-bot-api'; -import { generateOTP, saveOTP } from '@/lib/otp'; - -export async function POST() { - const token = process.env.TELEGRAM_BOT_TOKEN; - const chatId = process.env.TELEGRAM_CHAT_ID; - - if (!token || !chatId) { - console.error("Telegram credentials missing"); - return NextResponse.json({ error: 'Telegram not configured' }, { status: 500 }); - } - - const otp = generateOTP(); - saveOTP(otp); - - const bot = new TelegramBot(token, { polling: false }); - try { - await bot.sendMessage(chatId, `🔐 Your Login Code: ${otp}`); - return NextResponse.json({ success: true }); - } catch (error: any) { - console.error('Telegram Error:', error); - return NextResponse.json({ error: 'Failed to send message: ' + error.message }, { status: 500 }); - } -} diff --git a/app/api/auth/verify-otp/route.ts b/app/api/auth/verify-otp/route.ts new file mode 100644 index 0000000..9194b67 --- /dev/null +++ b/app/api/auth/verify-otp/route.ts @@ -0,0 +1,34 @@ +import { NextResponse } from 'next/server'; +import { findUser, saveUser, createSession } from '@/lib/auth'; +import { verifyOTP } from '@/lib/otp'; + +export async function POST(req: Request) { + try { + const { username, otp } = await req.json(); + const ip = req.headers.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown'; + + if (!verifyOTP(username, otp)) { + return NextResponse.json({ error: 'Código inválido o expirado' }, { status: 401 }); + } + + const user = findUser(username); + if (!user) { + return NextResponse.json({ error: 'Usuario no encontrado' }, { status: 404 }); + } + + // Add IP to known list if not exists + if (!user.knownIps.includes(ip) && ip !== 'unknown') { + user.knownIps.push(ip); + saveUser(user); + } + + // Login success + await createSession(user); + + return NextResponse.json({ success: true }); + + } catch (error) { + console.error('OTP Verify error:', error); + return NextResponse.json({ error: 'Error interno' }, { status: 500 }); + } +} diff --git a/app/api/auth/verify/route.ts b/app/api/auth/verify/route.ts deleted file mode 100644 index 4b37de1..0000000 --- a/app/api/auth/verify/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NextResponse } from 'next/server'; -import { verifyOTP } from '@/lib/otp'; -import { cookies } from 'next/headers'; - -export async function POST(req: Request) { - try { - const { code } = await req.json(); - - if (verifyOTP(code)) { - // Set cookie - cookies().set('auth_token', 'true', { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 7, // 1 week - path: '/' - }); - return NextResponse.json({ success: true }); - } - - return NextResponse.json({ error: 'Invalid Code' }, { status: 401 }); - } catch (error) { - return NextResponse.json({ error: 'Invalid Request' }, { status: 400 }); - } -} diff --git a/app/login/page.tsx b/app/login/page.tsx index 529f90d..47970fd 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -2,23 +2,43 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; -import { Lock, Send, Loader2 } from 'lucide-react'; +import { Lock, User, Key, ArrowRight, ShieldCheck, Loader2 } from 'lucide-react'; +import Link from 'next/link'; export default function LoginPage() { - const [step, setStep] = useState<'initial' | 'verify'>('initial'); - const [code, setCode] = useState(''); + const router = useRouter(); + const [step, setStep] = useState<'credentials' | 'otp'>('credentials'); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); - const router = useRouter(); + + const [formData, setFormData] = useState({ + username: '', + password: '', + otp: '' + }); - const sendCode = async () => { + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); setLoading(true); setError(''); + try { - const res = await fetch('/api/auth/send', { method: 'POST' }); + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: formData.username, password: formData.password }) + }); + const data = await res.json(); - if (!res.ok) throw new Error(data.error || 'Failed to send code'); - setStep('verify'); + + if (!res.ok) throw new Error(data.error || 'Error al iniciar sesión'); + + if (data.requireOtp) { + setStep('otp'); + } else { + router.push('/'); + router.refresh(); + } } catch (err: any) { setError(err.message); } finally { @@ -26,21 +46,24 @@ export default function LoginPage() { } }; - const verifyCode = async () => { + const handleVerifyOtp = async (e: React.FormEvent) => { + e.preventDefault(); setLoading(true); setError(''); + try { - const res = await fetch('/api/auth/verify', { + const res = await fetch('/api/auth/verify-otp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code }) + body: JSON.stringify({ username: formData.username, otp: formData.otp }) }); + const data = await res.json(); - if (!res.ok) throw new Error(data.error || 'Invalid code'); - - // Redirect + + if (!res.ok) throw new Error(data.error || 'Código incorrecto'); + router.push('/'); - router.refresh(); + router.refresh(); } catch (err: any) { setError(err.message); } finally { @@ -49,66 +72,121 @@ export default function LoginPage() { }; return ( -
-
-
-
- +
+
+ {/* Header */} +
+
+
-

Secure Access

-

- Finanzas Personales -

+

Bienvenido

+

Sistema de Finanzas Personales

- {error && ( -
- {error} -
- )} +
+ {error && ( +
+ {error} +
+ )} - {step === 'initial' ? ( -
-

- Click below to receive a login code via Telegram. -

- -
- ) : ( -
-

- Enter the 6-digit code sent to your Telegram. -

- setCode(e.target.value)} - placeholder="123456" - className="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-3 text-center text-2xl tracking-widest focus:outline-none focus:ring-2 focus:ring-blue-500" - maxLength={6} - /> - - -
- )} + {step === 'credentials' ? ( +
+
+ +
+ + setFormData({...formData, username: e.target.value})} + className="w-full pl-10 pr-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 outline-none transition-all placeholder:text-slate-600" + placeholder="Ingresa tu usuario" + /> +
+
+ +
+ +
+ + setFormData({...formData, password: e.target.value})} + className="w-full pl-10 pr-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 outline-none transition-all placeholder:text-slate-600" + placeholder="••••••••" + /> +
+
+ + +
+ ) : ( +
+
+
+ +
+

Verificación de Identidad

+

+ Hemos enviado un código a tu Telegram. +
Ingrésalo para continuar. +

+
+ +
+ setFormData({...formData, otp: e.target.value.replace(/\D/g, '')})} + className="w-full text-center text-2xl tracking-[0.5em] font-mono py-3 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all" + placeholder="000000" + /> +
+ + + + +
+ )} +
+ +
+

+ ¿No tienes cuenta?{' '} + + Regístrate aquí + +

+
); diff --git a/app/register/page.tsx b/app/register/page.tsx new file mode 100644 index 0000000..5cc68b1 --- /dev/null +++ b/app/register/page.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { UserPlus, User, Key, MessageSquare, Loader2 } from 'lucide-react'; +import Link from 'next/link'; + +export default function RegisterPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const [formData, setFormData] = useState({ + username: '', + password: '', + chatId: '' + }); + + const handleRegister = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const res = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) + }); + + const data = await res.json(); + + if (!res.ok) throw new Error(data.error || 'Error al registrarse'); + + router.push('/'); + router.refresh(); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Header */} +
+
+ +
+

Crear Cuenta

+

Configura tu acceso a Finanzas

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+
+ +
+ + setFormData({...formData, username: e.target.value})} + className="w-full pl-10 pr-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all placeholder:text-slate-600" + placeholder="Elige un usuario" + /> +
+
+ +
+ +
+ + setFormData({...formData, password: e.target.value})} + className="w-full pl-10 pr-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all placeholder:text-slate-600" + placeholder="Mínimo 6 caracteres" + minLength={6} + /> +
+
+ +
+ +
+ + setFormData({...formData, chatId: e.target.value})} + className="w-full pl-10 pr-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all placeholder:text-slate-600" + placeholder="Ej: 123456789" + /> +
+

+ ℹ️ + Envía /start a tu bot para obtener este ID. +

+
+ + +
+
+ +
+

+ ¿Ya tienes cuenta?{' '} + + Inicia Sesión + +

+
+
+
+ ); +} diff --git a/data/db.json b/data/db.json index d6f38e1..e9b3047 100644 --- a/data/db.json +++ b/data/db.json @@ -1,23 +1,6 @@ { "fixedDebts": [], - "variableDebts": [ - { - "name": "netflix", - "amount": 5000, - "date": "2026-01-29T00:00:00.000Z", - "category": "shopping", - "isPaid": false, - "id": "f2e424ca-5f07-4f57-9386-822354e0ee1e" - }, - { - "name": "youtube", - "amount": 2500, - "date": "2026-01-29T00:00:00.000Z", - "category": "shopping", - "isPaid": false, - "id": "621c3caf-529b-4c33-b46f-3d86b119dd75" - } - ], + "variableDebts": [], "creditCards": [], "cardPayments": [], "monthlyBudgets": [], diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..795cbc9 --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,84 @@ +import fs from 'fs'; +import path from 'path'; +import { User } from './types'; +import bcrypt from 'bcryptjs'; +import { SignJWT, jwtVerify } from 'jose'; +import { cookies } from 'next/headers'; + +const USERS_FILE = path.join(process.cwd(), 'data', 'users.json'); +const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET || 'fallback-secret-key-change-me'); + +// --- User Management --- + +export function getUsers(): User[] { + if (!fs.existsSync(USERS_FILE)) { + fs.writeFileSync(USERS_FILE, JSON.stringify([])); + return []; + } + try { + const data = fs.readFileSync(USERS_FILE, 'utf8'); + return JSON.parse(data); + } catch (e) { + return []; + } +} + +export function saveUser(user: User) { + const users = getUsers(); + const existingIndex = users.findIndex(u => u.username === user.username); + + if (existingIndex >= 0) { + users[existingIndex] = user; + } else { + users.push(user); + } + + fs.writeFileSync(USERS_FILE, JSON.stringify(users, null, 2)); +} + +export function findUser(username: string): User | undefined { + return getUsers().find(u => u.username === username); +} + +// --- Security Utils --- + +export async function hashPassword(password: string): Promise { + return await bcrypt.hash(password, 10); +} + +export async function verifyPassword(password: string, hash: string): Promise { + return await bcrypt.compare(password, hash); +} + +// --- Session Management (JWT) --- + +export async function createSession(user: User) { + const token = await new SignJWT({ username: user.username, chatId: user.chatId }) + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('7d') + .sign(SECRET_KEY); + + cookies().set('session', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7, // 7 days + path: '/', + }); +} + +export async function verifySession() { + const session = cookies().get('session')?.value; + if (!session) return null; + + try { + const { payload } = await jwtVerify(session, SECRET_KEY); + return payload as { username: string; chatId: string }; + } catch (error) { + return null; + } +} + +export function destroySession() { + cookies().delete('session'); +} diff --git a/lib/otp.ts b/lib/otp.ts index 8dbf6ef..dea61f5 100644 --- a/lib/otp.ts +++ b/lib/otp.ts @@ -8,32 +8,57 @@ interface OTPData { expiresAt: number; } -export function generateOTP(): string { - return Math.floor(100000 + Math.random() * 900000).toString(); -} +// Map username -> OTP Data +type OTPStore = Record; -export function saveOTP(code: string) { - const data: OTPData = { - code, - expiresAt: Date.now() + 5 * 60 * 1000 // 5 minutes - }; +function getStore(): OTPStore { + if (!fs.existsSync(OTP_FILE)) return {}; try { - fs.writeFileSync(OTP_FILE, JSON.stringify(data)); - } catch (err) { - console.error("Error saving OTP:", err); + return JSON.parse(fs.readFileSync(OTP_FILE, 'utf8')); + } catch { + return {}; } } -export function verifyOTP(code: string): boolean { - if (!fs.existsSync(OTP_FILE)) return false; - +function saveStore(store: OTPStore) { try { - const data: OTPData = JSON.parse(fs.readFileSync(OTP_FILE, 'utf8')); - if (Date.now() > data.expiresAt) return false; - // Simple check - return String(data.code).trim() === String(code).trim(); - } catch (e) { - console.error("Error verifying OTP:", e); + fs.writeFileSync(OTP_FILE, JSON.stringify(store)); + } catch (err) { + console.error("Error saving OTP store:", err); + } +} + +export function generateOTP(username: string): string { + const code = Math.floor(100000 + Math.random() * 900000).toString(); // 6 digits + const store = getStore(); + + store[username] = { + code, + expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes + }; + + saveStore(store); + return code; +} + +export function verifyOTP(username: string, code: string): boolean { + const store = getStore(); + const entry = store[username]; + + if (!entry) return false; + + if (Date.now() > entry.expiresAt) { + delete store[username]; + saveStore(store); return false; } + + // Compare strings safely + if (String(entry.code).trim() === String(code).trim()) { + delete store[username]; // Consume OTP so it can't be reused + saveStore(store); + return true; + } + + return false; } diff --git a/lib/types.ts b/lib/types.ts index 8ffcb8b..f783874 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -110,3 +110,12 @@ export interface AppSettings { } aiProviders: AIServiceConfig[] } + +// Auth Types +export interface User { + id: string + username: string + passwordHash: string + chatId: string + knownIps: string[] +} diff --git a/middleware.ts b/middleware.ts index 8afc815..07da3bb 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,34 +1,77 @@ -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { jwtVerify } from 'jose'; -export function middleware(request: NextRequest) { - const token = request.cookies.get('auth_token') - - // Public paths that don't require authentication +const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET || 'fallback-secret-key-change-me'); + +export async function middleware(request: NextRequest) { + const session = request.cookies.get('session')?.value; + const { pathname } = request.nextUrl; + + // Paths that are always public + const publicPaths = [ + '/login', + '/register', + '/api/auth/login', + '/api/auth/register', + '/api/auth/verify-otp', + '/api/auth/send' + ]; + + // Static assets and Next.js internals if ( - request.nextUrl.pathname.startsWith('/_next') || - request.nextUrl.pathname.startsWith('/static') || - request.nextUrl.pathname === '/login' || - request.nextUrl.pathname === '/favicon.ico' || - request.nextUrl.pathname.startsWith('/api/auth') + pathname.startsWith('/_next') || + pathname.startsWith('/static') || + pathname.includes('.') // images, icons, etc ) { - // If user is already logged in and tries to access login, redirect to dashboard - if (token && request.nextUrl.pathname === '/login') { - return NextResponse.redirect(new URL('/', request.url)) + return NextResponse.next(); + } + + const isPublic = publicPaths.some(path => pathname.startsWith(path)); + + // If user has session, try to verify it + let isValidSession = false; + if (session) { + try { + await jwtVerify(session, SECRET_KEY); + isValidSession = true; + } catch (e) { + isValidSession = false; } - return NextResponse.next() } - // If no token, redirect to login - if (!token) { - return NextResponse.redirect(new URL('/login', request.url)) + // Logic: + + // 1. If public path and Logged In -> Redirect to Dashboard + if (isPublic && isValidSession && (pathname === '/login' || pathname === '/register')) { + return NextResponse.redirect(new URL('/', request.url)); } - return NextResponse.next() + // 2. If public path -> Allow + if (isPublic) { + return NextResponse.next(); + } + + // 3. If protected path and Not Logged In -> Redirect to Login + if (!isValidSession) { + const loginUrl = new URL('/login', request.url); + // Optional: Add ?from=pathname to redirect back after login + return NextResponse.redirect(loginUrl); + } + + // 4. Protected path and Logged In -> Allow + return NextResponse.next(); } export const config = { matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (API routes) -> Wait, we WANT to protect API routes except auth ones + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + */ '/((?!_next/static|_next/image|favicon.ico).*)', ], -} +}; diff --git a/package-lock.json b/package-lock.json index 8c5a7c3..9be0afd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,13 @@ "@types/react-dom": "^19.2.3", "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.23", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "dotenv": "^17.2.3", + "headers-polyfill": "^4.0.3", + "jose": "^6.1.3", "lucide-react": "^0.563.0", "next": "^14.2.35", "node-telegram-bot-api": "^0.67.0", @@ -773,6 +776,15 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1942,6 +1954,12 @@ "node": ">= 0.4" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "license": "MIT" + }, "node_modules/http-signature": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", @@ -2401,6 +2419,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 958d0c7..9951e8a 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,13 @@ "@types/react-dom": "^19.2.3", "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.23", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "dotenv": "^17.2.3", + "headers-polyfill": "^4.0.3", + "jose": "^6.1.3", "lucide-react": "^0.563.0", "next": "^14.2.35", "node-telegram-bot-api": "^0.67.0",