hightjs 0.2.42 → 0.2.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idea/copilotDiffState.xml +67 -0
- package/README.md +52 -0
- package/dist/auth/core.js +3 -3
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/auth/providers/google.d.ts +63 -0
- package/dist/auth/providers/google.js +186 -0
- package/dist/auth/providers.d.ts +1 -0
- package/dist/auth/providers.js +3 -1
- package/dist/auth/types.d.ts +6 -7
- package/dist/client/entry.client.js +11 -1
- package/dist/hightweb-global.d.ts +0 -0
- package/dist/hightweb-global.js +1 -0
- package/dist/hotReload.d.ts +8 -1
- package/dist/hotReload.js +304 -144
- package/dist/index.d.ts +2 -1
- package/dist/index.js +20 -33
- package/dist/renderer.js +1 -1
- package/dist/router.d.ts +24 -1
- package/dist/router.js +202 -2
- package/dist/ssl/selfSigned.d.ts +0 -0
- package/dist/ssl/selfSigned.js +1 -0
- package/dist/types/websocket.d.ts +27 -0
- package/dist/types/websocket.js +2 -0
- package/dist/types.d.ts +19 -1
- package/package.json +1 -1
- package/src/auth/core.ts +3 -3
- package/src/auth/index.ts +2 -3
- package/src/auth/providers/google.ts +218 -0
- package/src/auth/providers.ts +1 -1
- package/src/auth/types.ts +3 -8
- package/src/client/entry.client.tsx +12 -1
- package/src/hightweb-global.ts +1 -0
- package/src/hotReload.ts +333 -147
- package/src/index.ts +58 -51
- package/src/renderer.tsx +1 -1
- package/src/router.ts +231 -3
- package/src/ssl/selfSigned.ts +2 -0
- package/src/types.ts +24 -1
- package/src/auth/example.ts +0 -115
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="CopilotDiffPersistence">
|
|
4
|
+
<option name="pendingDiffs">
|
|
5
|
+
<map>
|
|
6
|
+
<entry key="$PROJECT_DIR$/src/types/websocket.ts">
|
|
7
|
+
<value>
|
|
8
|
+
<PendingDiffInfo>
|
|
9
|
+
<option name="filePath" value="$PROJECT_DIR$/src/types/websocket.ts" />
|
|
10
|
+
<option name="updatedContent" value="import { IncomingMessage } from 'http'; import { WebSocket } from 'ws'; export interface WebSocketContext { ws: WebSocket; req: IncomingMessage; url: URL; params: Record<string, string>; query: Record<string, string>; send: (data: any) => void; close: (code?: number, reason?: string) => void; broadcast: (data: any, exclude?: WebSocket[]) => void; } export interface WebSocketHandler { (ctx: WebSocketContext): void | Promise<void>; } export interface WebSocketRoute { path: string; handler: WebSocketHandler; pathPattern?: RegExp; paramNames?: string[]; } export interface WebSocketServer { route: (path: string, handler: WebSocketHandler) => void; start: (port?: number) => void; broadcast: (data: any) => void; getConnections: () => WebSocket[]; }" />
|
|
11
|
+
</PendingDiffInfo>
|
|
12
|
+
</value>
|
|
13
|
+
</entry>
|
|
14
|
+
<entry key="$PROJECT_DIR$/src/websocket/adapter.ts">
|
|
15
|
+
<value>
|
|
16
|
+
<PendingDiffInfo>
|
|
17
|
+
<option name="filePath" value="$PROJECT_DIR$/src/websocket/adapter.ts" />
|
|
18
|
+
<option name="updatedContent" value="import { Server } from 'http'; import { WebSocketServer as WSServer } from 'ws'; import { HightWSServer } from './server'; import { WebSocketServer, WebSocketHandler } from '../types/websocket'; import Console, { Colors } from '../api/console'; export class WebSocketAdapter { private wsServer: HightWSServer; private httpServer: Server | null = null; private wss: WSServer | null = null; constructor() { this.wsServer = new HightWSServer(); } /** * Integra WebSocket com um servidor HTTP existente */ attachToHttpServer(httpServer: Server): void { this.httpServer = httpServer; this.wss = new WSServer({ server: httpServer, perMessageDeflate: false }); console.log(`${Colors.FgGreen}✓${Colors.Reset} WebSocket attached to HTTP server`); this.wss.on('connection', (ws, req) => { this.wsServer['handleConnection'](ws, req); }); this.wss.on('error', (error) => { console.error(`${Colors.FgRed}[WebSocket Server Error]${Colors.Reset}`, error); }); } /** * Adiciona uma rota WebSocket */ route(path: string, handler: WebSocketHandler): void { this.wsServer.route(path, handler); } /** * Faz broadcast para todas as conexões */ broadcast(data: any): void { this.wsServer.broadcast(data); } /** * Retorna todas as conexões ativas */ getConnections() { return this.wsServer.getConnections(); } /** * Inicia servidor WebSocket standalone */ startStandalone(port: number = 8080): void { this.wsServer.start(port); } }" />
|
|
19
|
+
</PendingDiffInfo>
|
|
20
|
+
</value>
|
|
21
|
+
</entry>
|
|
22
|
+
<entry key="$PROJECT_DIR$/src/websocket/client.ts">
|
|
23
|
+
<value>
|
|
24
|
+
<PendingDiffInfo>
|
|
25
|
+
<option name="filePath" value="$PROJECT_DIR$/src/websocket/client.ts" />
|
|
26
|
+
<option name="originalContent" value=" " />
|
|
27
|
+
<option name="updatedContent" value="import React from 'react'; export class HightWSClient { private ws: WebSocket | null = null; private url: string; private reconnectAttempts: number = 0; private maxReconnectAttempts: number = 5; private reconnectDelay: number = 1000; private listeners: Map<string, Function[]> = new Map(); constructor(url: string) { this.url = url; this.connect(); } private connect(): void { try { this.ws = new WebSocket(this.url); this.ws.onopen = (event) => { console.log('[HightWS] Connected to WebSocket server'); this.reconnectAttempts = 0; this.emit('open', event); }; this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); this.emit('message', data); } catch (error) { // Se não for JSON, envia como string this.emit('message', event.data); } }; this.ws.onclose = (event) => { console.log('[HightWS] Connection closed'); this.emit('close', event); this.attemptReconnect(); }; this.ws.onerror = (event) => { console.error('[HightWS] WebSocket error:', event); this.emit('error', event); }; } catch (error) { console.error('[HightWS] Failed to connect:', error); this.attemptReconnect(); } } private attemptReconnect(): void { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; console.log(`[HightWS] Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); setTimeout(() => { this.connect(); }, this.reconnectDelay * this.reconnectAttempts); } else { console.error('[HightWS] Max reconnection attempts reached'); this.emit('maxReconnectAttemptsReached'); } } public send(data: any): void { if (this.ws && this.ws.readyState === WebSocket.OPEN) { const message = typeof data === 'string' ? data : JSON.stringify(data); this.ws.send(message); } else { console.warn('[HightWS] WebSocket is not connected'); } } public on(event: string, callback: Function): void { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event)!.push(callback); } public off(event: string, callback?: Function): void { if (!this.listeners.has(event)) return; if (callback) { const callbacks = this.listeners.get(event)!; const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } } else { this.listeners.delete(event); } } private emit(event: string, data?: any): void { if (this.listeners.has(event)) { this.listeners.get(event)!.forEach(callback => { try { callback(data); } catch (error) { console.error(`[HightWS] Error in event listener for ${event}:`, error); } }); } } public close(): void { if (this.ws) { this.ws.close(); this.ws = null; } } public get readyState(): number { return this.ws ? this.ws.readyState : WebSocket.CLOSED; } public get isConnected(): boolean { return this.ws !== null && this.ws.readyState === WebSocket.OPEN; } } // Hook React para usar WebSocket export function useWebSocket(url: string) { const [client, setClient] = React.useState<HightWSClient | null>(null); const [isConnected, setIsConnected] = React.useState(false); const [lastMessage, setLastMessage] = React.useState<any>(null); React.useEffect(() => { const wsClient = new HightWSClient(url); wsClient.on('open', () => { setIsConnected(true); }); wsClient.on('close', () => { setIsConnected(false); }); wsClient.on('message', (data) => { setLastMessage(data); }); setClient(wsClient); return () => { wsClient.close(); }; }, [url]); const sendMessage = React.useCallback((data: any) => { if (client) { client.send(data); } }, [client]); return { client, isConnected, lastMessage, sendMessage }; }" />
|
|
28
|
+
</PendingDiffInfo>
|
|
29
|
+
</value>
|
|
30
|
+
</entry>
|
|
31
|
+
<entry key="$PROJECT_DIR$/src/websocket/example.ts">
|
|
32
|
+
<value>
|
|
33
|
+
<PendingDiffInfo>
|
|
34
|
+
<option name="filePath" value="$PROJECT_DIR$/src/websocket/example.ts" />
|
|
35
|
+
<option name="updatedContent" value="// Exemplo de uso do WebSocket no HightJS // ============================================ // BACKEND - Como criar rotas WebSocket // ============================================ // Em src/web/backend/websocket.ts (ou qualquer arquivo backend) import { ws } from 'hightjs/ws'; // Rota simples de chat ws('/chat', (ctx) => { console.log('Nova conexão no chat:', ctx.req.socket.remoteAddress); // Envia mensagem de boas-vindas ctx.send({ type: 'welcome', message: 'Bem-vindo ao chat!' }); // Escuta mensagens do cliente ctx.ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); // Faz broadcast da mensagem para todos os outros clientes ctx.broadcast({ type: 'message', user: message.user || 'Anônimo', text: message.text, timestamp: new Date().toISOString() }, [ctx.ws]); // Exclui o remetente } catch (error) { console.error('Erro ao processar mensagem:', error); } }); // Quando a conexão fechar ctx.ws.on('close', () => { console.log('Cliente desconectado do chat'); }); }); // Rota com parâmetros ws('/room/:roomId', (ctx) => { const { roomId } = ctx.params; console.log(`Nova conexão na sala: ${roomId}`); ctx.send({ type: 'joined', room: roomId, message: `Você entrou na sala ${roomId}` }); }); // Rota para notificações em tempo real ws('/notifications/:userId', (ctx) => { const { userId } = ctx.params; // Pode usar query params também const token = ctx.query.token; if (!token) { ctx.close(1008, 'Token obrigatório'); return; } console.log(`Usuário ${userId} conectado para notificações`); // Simula envio de notificação a cada 30 segundos const interval = setInterval(() => { ctx.send({ type: 'notification', title: 'Nova notificação', body: 'Você tem uma nova mensagem', timestamp: new Date().toISOString() }); }, 30000); ctx.ws.on('close', () => { clearInterval(interval); console.log(`Usuário ${userId} desconectado das notificações`); }); }); // ============================================ // FRONTEND - Como usar WebSocket no React // ============================================ // Em qualquer componente React import React from 'react'; import { HightWSClient, useWebSocket } from 'hightjs/ws'; // Método 1: Usando o hook useWebSocket (mais fácil) function ChatComponent() { const { client, isConnected, lastMessage, sendMessage } = useWebSocket('ws://localhost:8080/chat'); const [messages, setMessages] = React.useState([]); const [newMessage, setNewMessage] = React.useState(''); // Atualiza mensagens quando recebe uma nova React.useEffect(() => { if (lastMessage && lastMessage.type === 'message') { setMessages(prev => [...prev, lastMessage]); } }, [lastMessage]); const handleSendMessage = () => { if (newMessage.trim() && isConnected) { sendMessage({ user: 'Seu Nome', text: newMessage }); setNewMessage(''); } }; return ( <div> <div>Status: {isConnected ? 'Conectado' : 'Desconectado'}</div> <div style={{ height: '300px', overflow: 'auto', border: '1px solid #ccc' }}> {messages.map((msg, index) => ( <div key={index}> <strong>{msg.user}:</strong> {msg.text} <small> ({new Date(msg.timestamp).toLocaleTimeString()})</small> </div> ))} </div> <div> <input value={newMessage} onChange={(e) => setNewMessage(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()} placeholder="Digite sua mensagem..." /> <button onClick={handleSendMessage} disabled={!isConnected}> Enviar </button> </div> </div> ); } // Método 2: Usando a classe HightWSClient diretamente (mais controle) function NotificationsComponent() { const [client, setClient] = React.useState(null); const [notifications, setNotifications] = React.useState([]); const userId = '123'; // ID do usuário atual React.useEffect(() => { const wsClient = new HightWSClient(`ws://localhost:8080/notifications/${userId}?token=abc123`); wsClient.on('open', () => { console.log('Conectado às notificações'); }); wsClient.on('message', (data) => { if (data.type === 'notification') { setNotifications(prev => [data, ...prev.slice(0, 9)]); // Mantém só as 10 últimas // Pode mostrar notificação do browser também if (Notification.permission === 'granted') { new Notification(data.title, { body: data.body }); } } }); wsClient.on('close', () => { console.log('Desconectado das notificações'); }); setClient(wsClient); return () => { wsClient.close(); }; }, [userId]); return ( <div> <h3>Notificações em tempo real</h3> {notifications.map((notif, index) => ( <div key={index} style={{ padding: '10px', border: '1px solid #ddd', margin: '5px 0' }}> <strong>{notif.title}</strong> <p>{notif.body}</p> <small>{new Date(notif.timestamp).toLocaleString()}</small> </div> ))} </div> ); } // ============================================ // INICIANDO O SERVIDOR WEBSOCKET // ============================================ // No seu arquivo principal de servidor (onde você inicia o HightJS) import { createWebSocketServer } from 'hightjs/ws'; // Método 1: Servidor WebSocket independente const wsServer = createWebSocketServer(); wsServer.start(8080); // Inicia na porta 8080 // Método 2: Integrar com servidor HTTP existente (Express/Fastify) // Isso ainda precisa ser implementado nos adapters, mas a estrutura está pronta export default ChatComponent;" />
|
|
36
|
+
</PendingDiffInfo>
|
|
37
|
+
</value>
|
|
38
|
+
</entry>
|
|
39
|
+
<entry key="$PROJECT_DIR$/src/websocket/factory.ts">
|
|
40
|
+
<value>
|
|
41
|
+
<PendingDiffInfo>
|
|
42
|
+
<option name="filePath" value="$PROJECT_DIR$/src/websocket/factory.ts" />
|
|
43
|
+
<option name="updatedContent" value="import { HightWSServer } from './server'; import { WebSocketServer } from '../types/websocket'; /** * Cria uma nova instância do servidor WebSocket HightJS * @returns Uma nova instância do servidor WebSocket */ export function createWebSocketServer(): WebSocketServer { return new HightWSServer(); } /** * Instância global do servidor WebSocket (singleton) */ let globalWSServer: WebSocketServer | null = null; /** * Retorna a instância global do servidor WebSocket ou cria uma nova * @returns A instância global do servidor WebSocket */ export function getGlobalWebSocketServer(): WebSocketServer { if (!globalWSServer) { globalWSServer = new HightWSServer(); } return globalWSServer; } /** * Helper para criar rotas WebSocket rapidamente * @param path Caminho da rota * @param handler Função handler da rota */ export function ws(path: string, handler: (ctx: any) => void | Promise<void>) { const server = getGlobalWebSocketServer(); server.route(path, handler); return server; }" />
|
|
44
|
+
</PendingDiffInfo>
|
|
45
|
+
</value>
|
|
46
|
+
</entry>
|
|
47
|
+
<entry key="$PROJECT_DIR$/src/websocket/index.ts">
|
|
48
|
+
<value>
|
|
49
|
+
<PendingDiffInfo>
|
|
50
|
+
<option name="filePath" value="$PROJECT_DIR$/src/websocket/index.ts" />
|
|
51
|
+
<option name="originalContent" value=" " />
|
|
52
|
+
<option name="updatedContent" value="export { HightWSServer } from './server'; export { WebSocketContext, WebSocketHandler, WebSocketRoute, WebSocketServer } from '../types/websocket'; export { createWebSocketServer } from './factory'; export { HightWSClient, useWebSocket } from './client';" />
|
|
53
|
+
</PendingDiffInfo>
|
|
54
|
+
</value>
|
|
55
|
+
</entry>
|
|
56
|
+
<entry key="$PROJECT_DIR$/src/websocket/server.ts">
|
|
57
|
+
<value>
|
|
58
|
+
<PendingDiffInfo>
|
|
59
|
+
<option name="filePath" value="$PROJECT_DIR$/src/websocket/server.ts" />
|
|
60
|
+
<option name="updatedContent" value="import { WebSocket, WebSocketServer as WSServer } from 'ws'; import { IncomingMessage } from 'http'; import { URL } from 'url'; import { WebSocketContext, WebSocketHandler, WebSocketRoute, WebSocketServer } from '../types/websocket'; import Console, { Colors } from '../api/console'; export class HightWSServer implements WebSocketServer { private wss: WSServer | null = null; private routes: WebSocketRoute[] = []; private connections: Set<WebSocket> = new Set(); constructor() { this.setupRoutePatterns(); } route(path: string, handler: WebSocketHandler): void { const route: WebSocketRoute = { path, handler, ...this.createPathPattern(path) }; this.routes.push(route); console.log(`${Colors.FgBlue}[WebSocket]${Colors.Reset} Route registered: ${Colors.FgYellow}${path}${Colors.Reset}`); } start(port: number = 8080): void { this.wss = new WSServer({ port, perMessageDeflate: false }); console.log(`${Colors.FgGreen}✓${Colors.Reset} WebSocket server listening on port ${Colors.FgYellow}${port}${Colors.Reset}`); this.wss.on('connection', (ws: WebSocket, req: IncomingMessage) => { this.connections.add(ws); console.log(`${Colors.FgBlue}[WebSocket]${Colors.Reset} New connection from ${req.socket.remoteAddress}`); ws.on('close', () => { this.connections.delete(ws); console.log(`${Colors.FgBlue}[WebSocket]${Colors.Reset} Connection closed`); }); ws.on('error', (error) => { console.error(`${Colors.FgRed}[WebSocket Error]${Colors.Reset}`, error); this.connections.delete(ws); }); this.handleConnection(ws, req); }); this.wss.on('error', (error) => { console.error(`${Colors.FgRed}[WebSocket Server Error]${Colors.Reset}`, error); }); } broadcast(data: any): void { const message = typeof data === 'string' ? data : JSON.stringify(data); this.connections.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { ws.send(message); } }); } getConnections(): WebSocket[] { return Array.from(this.connections); } private handleConnection(ws: WebSocket, req: IncomingMessage): void { if (!req.url) return; const url = new URL(req.url, `http://${req.headers.host}`); const pathname = url.pathname; // Encontra a rota correspondente const matchedRoute = this.findMatchingRoute(pathname); if (!matchedRoute) { console.log(`${Colors.FgYellow}[WebSocket]${Colors.Reset} No route found for ${pathname}`); ws.close(1000, 'No route found'); return; } // Extrai parâmetros da URL const params = this.extractParams(pathname, matchedRoute); const query = Object.fromEntries(url.searchParams.entries()); // Cria o contexto const context: WebSocketContext = { ws, req, url, params, query, send: (data: any) => { if (ws.readyState === WebSocket.OPEN) { const message = typeof data === 'string' ? data : JSON.stringify(data); ws.send(message); } }, close: (code?: number, reason?: string) => { ws.close(code || 1000, reason); }, broadcast: (data: any, exclude?: WebSocket[]) => { const message = typeof data === 'string' ? data : JSON.stringify(data); const excludeSet = new Set(exclude || []); this.connections.forEach(connection => { if (connection.readyState === WebSocket.OPEN && !excludeSet.has(connection)) { connection.send(message); } }); } }; try { matchedRoute.handler(context); } catch (error) { console.error(`${Colors.FgRed}[WebSocket Handler Error]${Colors.Reset}`, error); ws.close(1011, 'Internal server error'); } } private createPathPattern(path: string): { pathPattern: RegExp; paramNames: string[] } { const paramNames: string[] = []; const pattern = path .replace(/\//g, '\\/') .replace(/:([^\/]+)/g, (_, paramName) => { paramNames.push(paramName); return '([^/]+)'; }) .replace(/\*/g, '.*'); return { pathPattern: new RegExp(`^${pattern}$`), paramNames }; } private findMatchingRoute(pathname: string): WebSocketRoute | null { return this.routes.find(route => { if (route.pathPattern) { return route.pathPattern.test(pathname); } return route.path === pathname; }) || null; } private extractParams(pathname: string, route: WebSocketRoute): Record<string, string> { const params: Record<string, string> = {}; if (route.pathPattern && route.paramNames) { const match = pathname.match(route.pathPattern); if (match) { route.paramNames.forEach((paramName, index) => { params[paramName] = match[index + 1]; }); } } return params; } private setupRoutePatterns(): void { // Reprocessa todos os patterns das rotas existentes this.routes.forEach(route => { const { pathPattern, paramNames } = this.createPathPattern(route.path); route.pathPattern = pathPattern; route.paramNames = paramNames; }); } }" />
|
|
61
|
+
</PendingDiffInfo>
|
|
62
|
+
</value>
|
|
63
|
+
</entry>
|
|
64
|
+
</map>
|
|
65
|
+
</option>
|
|
66
|
+
</component>
|
|
67
|
+
</project>
|
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Caso tenha alguma dúvida, entre em contato por uma das redes abaixo:
|
|
|
23
23
|
- [📦 Estrutura Recomendada](#-estrutura-recomendada)
|
|
24
24
|
- [🖥️ Rotas Frontend (Páginas)](#-rotas-frontend)
|
|
25
25
|
- [🌐 Rotas Backend (API)](#-rotas-backend)
|
|
26
|
+
- [🛜 WebSocket](#-websocket)
|
|
26
27
|
- [🧩 Middlewares](#-middlewares)
|
|
27
28
|
- [🔐 Autenticação (HightJS/auth)](#-autenticação-hightjsauth)
|
|
28
29
|
- [🛠️ CLI](#-cli)
|
|
@@ -38,6 +39,10 @@ Caso tenha alguma dúvida, entre em contato por uma das redes abaixo:
|
|
|
38
39
|
## ✨ Principais Recursos
|
|
39
40
|
|
|
40
41
|
- **Roteamento automático** de páginas [`src/web/routes`] e APIs [`src/web/backend/routes`]
|
|
42
|
+
- **React 19** com client-side hydration
|
|
43
|
+
- **TypeScript** first (totalmente tipado)
|
|
44
|
+
- **WebSockets** nativo nas rotas backend
|
|
45
|
+
- **Rotas dinâmicas** com parâmetros (frontend e backend)
|
|
41
46
|
- **Middlewares** por pasta ou rota
|
|
42
47
|
- **Hot Reload** nativo (WebSocket interno) em dev
|
|
43
48
|
- **Layouts globais** e página 404 customizada
|
|
@@ -257,6 +262,53 @@ export default route;
|
|
|
257
262
|
|
|
258
263
|
---
|
|
259
264
|
|
|
265
|
+
## 🛜 WebSocket
|
|
266
|
+
|
|
267
|
+
O HightJS possui suporte nativo a WebSockets nas rotas do backend.
|
|
268
|
+
|
|
269
|
+
### Exemplo Prático
|
|
270
|
+
|
|
271
|
+
`src/web/backend/routes/chat.ts`:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import {BackendRouteConfig, HightJSResponse} from 'hightjs';
|
|
275
|
+
|
|
276
|
+
const route: BackendRouteConfig = {
|
|
277
|
+
pattern: '/api/chat',
|
|
278
|
+
GET: async () => {
|
|
279
|
+
return HightJSResponse.json({ message: 'Chat HTTP endpoint' });
|
|
280
|
+
},
|
|
281
|
+
WS: (ctx) => {
|
|
282
|
+
console.log('Nova conexão WebSocket no chat');
|
|
283
|
+
|
|
284
|
+
ctx.send({
|
|
285
|
+
type: 'welcome',
|
|
286
|
+
message: 'Bem-vindo ao chat!'
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
ctx.ws.on('message', (data) => {
|
|
290
|
+
const message = JSON.parse(data.toString());
|
|
291
|
+
console.log(message)
|
|
292
|
+
// Broadcast para todos exceto o remetente
|
|
293
|
+
ctx.broadcast({
|
|
294
|
+
type: 'message',
|
|
295
|
+
user: message.user,
|
|
296
|
+
text: message.text,
|
|
297
|
+
timestamp: new Date().toISOString()
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
ctx.ws.on('close', () => {
|
|
302
|
+
console.log('Cliente desconectado');
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
export default route;
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
260
312
|
## 🧩 Middlewares
|
|
261
313
|
|
|
262
314
|
Adicione middlewares:
|
package/dist/auth/core.js
CHANGED
|
@@ -55,7 +55,7 @@ class HWebAuth {
|
|
|
55
55
|
const sessionResult = this.sessionManager.createSession(user);
|
|
56
56
|
// Callback de sessão se definido
|
|
57
57
|
if (this.config.callbacks?.session) {
|
|
58
|
-
sessionResult.session = await this.config.callbacks.session(sessionResult.session, user);
|
|
58
|
+
sessionResult.session = await this.config.callbacks.session({ session: sessionResult.session, user, provider: providerId });
|
|
59
59
|
}
|
|
60
60
|
return sessionResult;
|
|
61
61
|
}
|
|
@@ -86,7 +86,7 @@ class HWebAuth {
|
|
|
86
86
|
.clearCookie('hweb-auth-token', {
|
|
87
87
|
path: '/',
|
|
88
88
|
httpOnly: true,
|
|
89
|
-
secure:
|
|
89
|
+
secure: this.config.secureCookies || false,
|
|
90
90
|
sameSite: 'strict'
|
|
91
91
|
});
|
|
92
92
|
}
|
|
@@ -143,7 +143,7 @@ class HWebAuth {
|
|
|
143
143
|
.json(data)
|
|
144
144
|
.cookie('hweb-auth-token', token, {
|
|
145
145
|
httpOnly: true,
|
|
146
|
-
secure:
|
|
146
|
+
secure: this.config.secureCookies || false, // Always secure, even in development
|
|
147
147
|
sameSite: 'strict', // Prevent CSRF attacks
|
|
148
148
|
maxAge: (this.config.session?.maxAge || 86400) * 1000,
|
|
149
149
|
path: '/',
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export * from './providers';
|
|
|
3
3
|
export * from './core';
|
|
4
4
|
export * from './routes';
|
|
5
5
|
export * from './jwt';
|
|
6
|
-
export { CredentialsProvider, DiscordProvider } from './providers';
|
|
6
|
+
export { CredentialsProvider, DiscordProvider, GoogleProvider } from './providers';
|
|
7
7
|
export { createAuthRoutes } from './routes';
|
package/dist/auth/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.createAuthRoutes = exports.DiscordProvider = exports.CredentialsProvider = void 0;
|
|
17
|
+
exports.createAuthRoutes = exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
|
|
18
18
|
// Exportações principais do sistema de autenticação
|
|
19
19
|
__exportStar(require("./types"), exports);
|
|
20
20
|
__exportStar(require("./providers"), exports);
|
|
@@ -24,5 +24,6 @@ __exportStar(require("./jwt"), exports);
|
|
|
24
24
|
var providers_1 = require("./providers");
|
|
25
25
|
Object.defineProperty(exports, "CredentialsProvider", { enumerable: true, get: function () { return providers_1.CredentialsProvider; } });
|
|
26
26
|
Object.defineProperty(exports, "DiscordProvider", { enumerable: true, get: function () { return providers_1.DiscordProvider; } });
|
|
27
|
+
Object.defineProperty(exports, "GoogleProvider", { enumerable: true, get: function () { return providers_1.GoogleProvider; } });
|
|
27
28
|
var routes_1 = require("./routes");
|
|
28
29
|
Object.defineProperty(exports, "createAuthRoutes", { enumerable: true, get: function () { return routes_1.createAuthRoutes; } });
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AuthProviderClass, AuthRoute, User } from '../types';
|
|
2
|
+
export interface GoogleConfig {
|
|
3
|
+
id?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
callbackUrl?: string;
|
|
8
|
+
successUrl?: string;
|
|
9
|
+
scope?: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Provider para autenticação com Google OAuth2
|
|
13
|
+
*
|
|
14
|
+
* Este provider permite autenticação usando Google OAuth2.
|
|
15
|
+
* Automaticamente gerencia o fluxo OAuth completo e rotas necessárias.
|
|
16
|
+
*
|
|
17
|
+
* Exemplo de uso:
|
|
18
|
+
* ```typescript
|
|
19
|
+
* new GoogleProvider({
|
|
20
|
+
* clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
21
|
+
* clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
22
|
+
* callbackUrl: "http://localhost:3000/api/auth/callback/google"
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Fluxo de autenticação:
|
|
27
|
+
* 1. GET /api/auth/signin/google - Gera URL e redireciona para Google
|
|
28
|
+
* 2. Google redireciona para /api/auth/callback/google com código
|
|
29
|
+
* 3. Provider troca código por token e busca dados do usuário
|
|
30
|
+
* 4. Retorna objeto User com dados do Google
|
|
31
|
+
*/
|
|
32
|
+
export declare class GoogleProvider implements AuthProviderClass {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly type: string;
|
|
36
|
+
private config;
|
|
37
|
+
private readonly defaultScope;
|
|
38
|
+
constructor(config: GoogleConfig);
|
|
39
|
+
/**
|
|
40
|
+
* Método para gerar URL OAuth (usado pelo handleSignIn)
|
|
41
|
+
*/
|
|
42
|
+
handleOauth(credentials?: Record<string, string>): string;
|
|
43
|
+
/**
|
|
44
|
+
* Método principal - redireciona para OAuth ou processa o callback
|
|
45
|
+
*/
|
|
46
|
+
handleSignIn(credentials: Record<string, string>): Promise<User | string | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Processa o callback do OAuth (troca o código pelo usuário)
|
|
49
|
+
*/
|
|
50
|
+
private processOAuthCallback;
|
|
51
|
+
/**
|
|
52
|
+
* Rotas adicionais específicas do Google OAuth
|
|
53
|
+
*/
|
|
54
|
+
additionalRoutes: AuthRoute[];
|
|
55
|
+
/**
|
|
56
|
+
* Gera a URL de autorização do Google
|
|
57
|
+
*/
|
|
58
|
+
getAuthorizationUrl(): string;
|
|
59
|
+
/**
|
|
60
|
+
* Retorna a configuração pública do provider
|
|
61
|
+
*/
|
|
62
|
+
getConfig(): any;
|
|
63
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GoogleProvider = void 0;
|
|
4
|
+
const http_1 = require("../../api/http");
|
|
5
|
+
/**
|
|
6
|
+
* Provider para autenticação com Google OAuth2
|
|
7
|
+
*
|
|
8
|
+
* Este provider permite autenticação usando Google OAuth2.
|
|
9
|
+
* Automaticamente gerencia o fluxo OAuth completo e rotas necessárias.
|
|
10
|
+
*
|
|
11
|
+
* Exemplo de uso:
|
|
12
|
+
* ```typescript
|
|
13
|
+
* new GoogleProvider({
|
|
14
|
+
* clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
15
|
+
* clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
16
|
+
* callbackUrl: "http://localhost:3000/api/auth/callback/google"
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Fluxo de autenticação:
|
|
21
|
+
* 1. GET /api/auth/signin/google - Gera URL e redireciona para Google
|
|
22
|
+
* 2. Google redireciona para /api/auth/callback/google com código
|
|
23
|
+
* 3. Provider troca código por token e busca dados do usuário
|
|
24
|
+
* 4. Retorna objeto User com dados do Google
|
|
25
|
+
*/
|
|
26
|
+
class GoogleProvider {
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.type = 'google';
|
|
29
|
+
this.defaultScope = [
|
|
30
|
+
'openid',
|
|
31
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
32
|
+
'https://www.googleapis.com/auth/userinfo.profile'
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Rotas adicionais específicas do Google OAuth
|
|
36
|
+
*/
|
|
37
|
+
this.additionalRoutes = [
|
|
38
|
+
// Rota de callback do Google
|
|
39
|
+
{
|
|
40
|
+
method: 'GET',
|
|
41
|
+
path: '/api/auth/callback/google',
|
|
42
|
+
handler: async (req, params) => {
|
|
43
|
+
const url = new URL(req.url || '', 'http://localhost');
|
|
44
|
+
const code = url.searchParams.get('code');
|
|
45
|
+
if (!code) {
|
|
46
|
+
return http_1.HightJSResponse.json({ error: 'Authorization code not provided' }, { status: 400 });
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
// Delega o 'code' para o endpoint de signin principal
|
|
50
|
+
const authResponse = await fetch(`${req.headers.origin || 'http://localhost:3000'}/api/auth/signin`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
provider: this.id,
|
|
57
|
+
code,
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
if (authResponse.ok) {
|
|
61
|
+
// Propaga o cookie de sessão e redireciona para a URL de sucesso
|
|
62
|
+
const setCookieHeader = authResponse.headers.get('set-cookie');
|
|
63
|
+
if (this.config.successUrl) {
|
|
64
|
+
return http_1.HightJSResponse
|
|
65
|
+
.redirect(this.config.successUrl)
|
|
66
|
+
.header('Set-Cookie', setCookieHeader || '');
|
|
67
|
+
}
|
|
68
|
+
return http_1.HightJSResponse.json({ success: true })
|
|
69
|
+
.header('Set-Cookie', setCookieHeader || '');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const errorText = await authResponse.text();
|
|
73
|
+
console.error(`[${this.id} Provider] Session creation failed during callback. Status: ${authResponse.status}, Body: ${errorText}`);
|
|
74
|
+
return http_1.HightJSResponse.json({ error: 'Session creation failed' }, { status: 500 });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error(`[${this.id} Provider] Callback handler fetch error:`, error);
|
|
79
|
+
return http_1.HightJSResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
this.config = config;
|
|
85
|
+
this.id = config.id || 'google';
|
|
86
|
+
this.name = config.name || 'Google';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Método para gerar URL OAuth (usado pelo handleSignIn)
|
|
90
|
+
*/
|
|
91
|
+
handleOauth(credentials = {}) {
|
|
92
|
+
return this.getAuthorizationUrl();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Método principal - redireciona para OAuth ou processa o callback
|
|
96
|
+
*/
|
|
97
|
+
async handleSignIn(credentials) {
|
|
98
|
+
// Se tem código, é o callback - processa a autenticação
|
|
99
|
+
if (credentials.code) {
|
|
100
|
+
return await this.processOAuthCallback(credentials);
|
|
101
|
+
}
|
|
102
|
+
// Se não tem código, é o início do OAuth - retorna a URL
|
|
103
|
+
return this.handleOauth(credentials);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Processa o callback do OAuth (troca o código pelo usuário)
|
|
107
|
+
*/
|
|
108
|
+
async processOAuthCallback(credentials) {
|
|
109
|
+
try {
|
|
110
|
+
const { code } = credentials;
|
|
111
|
+
if (!code) {
|
|
112
|
+
throw new Error('Authorization code not provided');
|
|
113
|
+
}
|
|
114
|
+
// Troca o código por um access token
|
|
115
|
+
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
119
|
+
},
|
|
120
|
+
body: new URLSearchParams({
|
|
121
|
+
client_id: this.config.clientId,
|
|
122
|
+
client_secret: this.config.clientSecret,
|
|
123
|
+
grant_type: 'authorization_code',
|
|
124
|
+
code,
|
|
125
|
+
redirect_uri: this.config.callbackUrl || '',
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
if (!tokenResponse.ok) {
|
|
129
|
+
const error = await tokenResponse.text();
|
|
130
|
+
throw new Error(`Failed to exchange code for token: ${error}`);
|
|
131
|
+
}
|
|
132
|
+
const tokens = await tokenResponse.json();
|
|
133
|
+
// Busca os dados do usuário com o access token
|
|
134
|
+
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
|
135
|
+
headers: {
|
|
136
|
+
'Authorization': `Bearer ${tokens.access_token}`,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
if (!userResponse.ok) {
|
|
140
|
+
throw new Error('Failed to fetch user data');
|
|
141
|
+
}
|
|
142
|
+
const googleUser = await userResponse.json();
|
|
143
|
+
// Retorna o objeto User padronizado
|
|
144
|
+
return {
|
|
145
|
+
id: googleUser.id,
|
|
146
|
+
name: googleUser.name,
|
|
147
|
+
email: googleUser.email,
|
|
148
|
+
image: googleUser.picture || null,
|
|
149
|
+
provider: this.id,
|
|
150
|
+
providerId: googleUser.id,
|
|
151
|
+
accessToken: tokens.access_token,
|
|
152
|
+
refreshToken: tokens.refresh_token
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error(`[${this.id} Provider] Error during OAuth callback:`, error);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Gera a URL de autorização do Google
|
|
162
|
+
*/
|
|
163
|
+
getAuthorizationUrl() {
|
|
164
|
+
const params = new URLSearchParams({
|
|
165
|
+
client_id: this.config.clientId,
|
|
166
|
+
redirect_uri: this.config.callbackUrl || '',
|
|
167
|
+
response_type: 'code',
|
|
168
|
+
scope: (this.config.scope || this.defaultScope).join(' ')
|
|
169
|
+
});
|
|
170
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Retorna a configuração pública do provider
|
|
174
|
+
*/
|
|
175
|
+
getConfig() {
|
|
176
|
+
return {
|
|
177
|
+
id: this.id,
|
|
178
|
+
name: this.name,
|
|
179
|
+
type: this.type,
|
|
180
|
+
clientId: this.config.clientId, // Público
|
|
181
|
+
scope: this.config.scope || this.defaultScope,
|
|
182
|
+
callbackUrl: this.config.callbackUrl
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.GoogleProvider = GoogleProvider;
|
package/dist/auth/providers.d.ts
CHANGED
package/dist/auth/providers.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DiscordProvider = exports.CredentialsProvider = void 0;
|
|
3
|
+
exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
|
|
4
4
|
// Exportações dos providers
|
|
5
5
|
var credentials_1 = require("./providers/credentials");
|
|
6
6
|
Object.defineProperty(exports, "CredentialsProvider", { enumerable: true, get: function () { return credentials_1.CredentialsProvider; } });
|
|
7
7
|
var discord_1 = require("./providers/discord");
|
|
8
8
|
Object.defineProperty(exports, "DiscordProvider", { enumerable: true, get: function () { return discord_1.DiscordProvider; } });
|
|
9
|
+
var google_1 = require("./providers/google");
|
|
10
|
+
Object.defineProperty(exports, "GoogleProvider", { enumerable: true, get: function () { return google_1.GoogleProvider; } });
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -48,7 +48,11 @@ export interface AuthConfig {
|
|
|
48
48
|
};
|
|
49
49
|
callbacks?: {
|
|
50
50
|
signIn?: (user: User, account: any, profile: any) => boolean | Promise<boolean>;
|
|
51
|
-
session?: (session
|
|
51
|
+
session?: ({ session, user, provider }: {
|
|
52
|
+
session: Session;
|
|
53
|
+
user: User;
|
|
54
|
+
provider: string;
|
|
55
|
+
}) => Session | Promise<Session>;
|
|
52
56
|
jwt?: (token: any, user: User, account: any, profile: any) => any | Promise<any>;
|
|
53
57
|
};
|
|
54
58
|
session?: {
|
|
@@ -58,12 +62,7 @@ export interface AuthConfig {
|
|
|
58
62
|
};
|
|
59
63
|
secret?: string;
|
|
60
64
|
debug?: boolean;
|
|
61
|
-
|
|
62
|
-
export interface AuthProvider {
|
|
63
|
-
id: string;
|
|
64
|
-
name: string;
|
|
65
|
-
type: 'credentials';
|
|
66
|
-
authorize?: (credentials: Record<string, string>) => Promise<User | null> | User | null;
|
|
65
|
+
secureCookies?: boolean;
|
|
67
66
|
}
|
|
68
67
|
export interface CredentialsConfig {
|
|
69
68
|
id?: string;
|
|
@@ -49,7 +49,17 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
|
|
|
49
49
|
const [params, setParams] = (0, react_1.useState)(initialParams);
|
|
50
50
|
const findRouteForPath = (0, react_1.useCallback)((path) => {
|
|
51
51
|
for (const route of routes) {
|
|
52
|
-
const regexPattern = route.pattern
|
|
52
|
+
const regexPattern = route.pattern
|
|
53
|
+
// [[...param]] → opcional catch-all
|
|
54
|
+
.replace(/\[\[\.\.\.(\w+)\]\]/g, '(?<$1>.+)?')
|
|
55
|
+
// [...param] → obrigatório catch-all
|
|
56
|
+
.replace(/\[\.\.\.(\w+)\]/g, '(?<$1>.+)')
|
|
57
|
+
// /[[param]] → opcional com barra também opcional
|
|
58
|
+
.replace(/\/\[\[(\w+)\]\]/g, '(?:/(?<$1>[^/]+))?')
|
|
59
|
+
// [[param]] → segmento opcional (sem barra anterior)
|
|
60
|
+
.replace(/\[\[(\w+)\]\]/g, '(?<$1>[^/]+)?')
|
|
61
|
+
// [param] → segmento obrigatório
|
|
62
|
+
.replace(/\[(\w+)\]/g, '(?<$1>[^/]+)');
|
|
53
63
|
const regex = new RegExp(`^${regexPattern}/?$`);
|
|
54
64
|
const match = path.match(regex);
|
|
55
65
|
if (match) {
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
package/dist/hotReload.d.ts
CHANGED
|
@@ -4,14 +4,19 @@ export declare class HotReloadManager {
|
|
|
4
4
|
private watchers;
|
|
5
5
|
private projectDir;
|
|
6
6
|
private clients;
|
|
7
|
-
private pingInterval;
|
|
8
7
|
private backendApiChangeCallback;
|
|
9
8
|
private frontendChangeCallback;
|
|
9
|
+
private isShuttingDown;
|
|
10
|
+
private debounceTimers;
|
|
11
|
+
private customHotReloadListener;
|
|
10
12
|
constructor(projectDir: string);
|
|
11
13
|
start(): Promise<void>;
|
|
12
14
|
handleUpgrade(request: IncomingMessage, socket: any, head: Buffer): void;
|
|
13
15
|
private setupWebSocketServer;
|
|
16
|
+
private cleanupClient;
|
|
14
17
|
private setupWatchers;
|
|
18
|
+
private debounce;
|
|
19
|
+
private handleAnySrcChange;
|
|
15
20
|
private notifyClients;
|
|
16
21
|
private restartServer;
|
|
17
22
|
stop(): void;
|
|
@@ -19,5 +24,7 @@ export declare class HotReloadManager {
|
|
|
19
24
|
private clearBackendCache;
|
|
20
25
|
onBackendApiChange(callback: () => void): void;
|
|
21
26
|
onFrontendChange(callback: () => void): void;
|
|
27
|
+
setHotReloadListener(listener: (file: string) => Promise<void> | void): void;
|
|
28
|
+
removeHotReloadListener(): void;
|
|
22
29
|
private checkFrontendBuild;
|
|
23
30
|
}
|