feat: add Layer 2 - WhatsApp Core logic, API Gateway models/auth, Frontend core

WhatsApp Core:
- SessionManager with Baileys integration for multi-account support
- Express server with REST API and Socket.IO for real-time events
- Session lifecycle management (create, disconnect, delete)
- Message sending with support for text, image, document, audio, video

API Gateway:
- Database models: User, WhatsAppAccount, Contact, Conversation, Message
- JWT authentication with access/refresh tokens
- Auth endpoints: login, refresh, register, me
- Pydantic schemas for request/response validation

Frontend:
- React 18 app structure with routing
- Zustand auth store with persistence
- API client with automatic token handling
- Base CSS and TypeScript declarations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude AI
2026-01-29 09:55:10 +00:00
parent 31d68bc118
commit 7042aa2061
19 changed files with 827 additions and 0 deletions

33
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,33 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import { useAuthStore } from './store/auth';
// Placeholder components - will be replaced
const LoginPage = () => <div>Login Page</div>;
const DashboardPage = () => <div>Dashboard</div>;
const MainLayout = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
function PrivateRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
return isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />;
}
export default function App() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/*"
element={
<PrivateRoute>
<MainLayout>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={<DashboardPage />} />
</Routes>
</MainLayout>
</PrivateRoute>
}
/>
</Routes>
);
}

View File

@@ -0,0 +1,79 @@
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
interface RequestOptions extends RequestInit {
params?: Record<string, string>;
}
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private getAuthHeaders(): Record<string, string> {
const token = localStorage.getItem('access_token');
return token ? { Authorization: `Bearer ${token}` } : {};
}
private async request<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
const { params, ...fetchOptions } = options;
let url = `${this.baseUrl}${endpoint}`;
if (params) {
const searchParams = new URLSearchParams(params);
url += `?${searchParams.toString()}`;
}
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...this.getAuthHeaders(),
...(options.headers as Record<string, string>),
};
const response = await fetch(url, {
...fetchOptions,
headers,
});
if (response.status === 401) {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login';
throw new Error('Unauthorized');
}
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Request failed' }));
throw new Error(error.detail || 'Request failed');
}
return response.json();
}
get<T>(endpoint: string, options?: RequestOptions): Promise<T> {
return this.request<T>(endpoint, { ...options, method: 'GET' });
}
post<T>(endpoint: string, data?: unknown, options?: RequestOptions): Promise<T> {
return this.request<T>(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data),
});
}
put<T>(endpoint: string, data?: unknown, options?: RequestOptions): Promise<T> {
return this.request<T>(endpoint, {
...options,
method: 'PUT',
body: JSON.stringify(data),
});
}
delete<T>(endpoint: string, options?: RequestOptions): Promise<T> {
return this.request<T>(endpoint, { ...options, method: 'DELETE' });
}
}
export const apiClient = new ApiClient(API_BASE_URL);

15
frontend/src/index.css Normal file
View File

@@ -0,0 +1,15 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
min-height: 100vh;
}

29
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,29 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ConfigProvider } from 'antd';
import esES from 'antd/locale/es_ES';
import App from './App';
import './index.css';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
},
},
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ConfigProvider locale={esES}>
<BrowserRouter>
<App />
</BrowserRouter>
</ConfigProvider>
</QueryClientProvider>
</React.StrictMode>
);

View File

@@ -0,0 +1,54 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface User {
id: string;
email: string;
name: string;
role: string;
status: string;
}
interface AuthState {
user: User | null;
accessToken: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
setAuth: (user: User, accessToken: string, refreshToken: string) => void;
logout: () => void;
updateUser: (user: Partial<User>) => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
accessToken: null,
refreshToken: null,
isAuthenticated: false,
setAuth: (user, accessToken, refreshToken) => {
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
set({ user, accessToken, refreshToken, isAuthenticated: true });
},
logout: () => {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
set({ user: null, accessToken: null, refreshToken: null, isAuthenticated: false });
},
updateUser: (userData) =>
set((state) => ({
user: state.user ? { ...state.user, ...userData } : null,
})),
}),
{
name: 'auth-storage',
partialize: (state) => ({
user: state.user,
accessToken: state.accessToken,
refreshToken: state.refreshToken,
isAuthenticated: state.isAuthenticated,
}),
}
)
);

10
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_WS_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}