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' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ ¿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}
+
+ )}
+
+
+
+
+
+
+ ¿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",