feat: estrutura inicial do gerenciador de subdomínios

- backend Express com endpoints Hostinger DNS API
- frontend React+Vite+Tailwind com card de API key
- docker-compose (frontend:3014, backend:8019, nginx:8021)
- Traefik label para subdominio.clube67.com
This commit is contained in:
VPS 4 Builder
2026-05-15 07:20:19 +02:00
commit 7a3b421ab2
14 changed files with 363 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
FROM node:20-alpine AS builder
WORKDIR /app/frontend
COPY package*.json .
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
EXPOSE 3014
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gerenciador de Subdomínios — Clube67</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
{
"name": "subdominio-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.0",
"@vitejs/plugin-react": "^5.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"tailwindcss": "^4.2.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}
+140
View File
@@ -0,0 +1,140 @@
import React, { useState, useEffect } from 'react';
const API = '/api';
export default function App() {
const [hasKey, setHasKey] = useState<boolean | null>(null);
const [apiKeyInput, setApiKeyInput] = useState('');
const [saving, setSaving] = useState(false);
const [subdomains, setSubdomains] = useState<any[]>([]);
const [loadingDomains, setLoadingDomains] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
useEffect(() => {
fetch(`${API}/config`)
.then(r => r.json())
.then(d => setHasKey(d.has_api_key))
.catch(() => setHasKey(false));
}, []);
async function saveKey() {
if (!apiKeyInput.trim()) return;
setSaving(true);
setError('');
try {
const r = await fetch(`${API}/config/hostinger-key`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ api_key: apiKeyInput })
});
if (r.ok) { setHasKey(true); setApiKeyInput(''); setSuccess('Chave salva com sucesso.'); }
else { const d = await r.json(); setError(d.error || 'Erro ao salvar.'); }
} finally { setSaving(false); }
}
async function fetchSubdomains() {
setLoadingDomains(true);
setError('');
try {
const r = await fetch(`${API}/subdomains`);
const d = await r.json();
if (r.ok) setSubdomains(Array.isArray(d) ? d : d.data || []);
else setError(d.error || 'Erro ao buscar domínios.');
} finally { setLoadingDomains(false); }
}
return (
<div className="min-h-screen bg-gray-950 text-white p-6">
<div className="max-w-2xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-orange-500 flex items-center justify-center font-black text-lg">C</div>
<div>
<h1 className="font-black text-xl tracking-tight uppercase">Gerenciador de Subdomínios</h1>
<p className="text-gray-400 text-sm">clube67.com Hostinger DNS</p>
</div>
</div>
{/* Card API Key */}
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="font-bold text-sm uppercase tracking-widest text-gray-400">Hostinger API Key</h2>
{hasKey === true && (
<span className="text-xs font-bold bg-green-900/50 text-green-400 border border-green-800 px-3 py-1 rounded-full">
CONFIGURADA
</span>
)}
{hasKey === false && (
<span className="text-xs font-bold bg-red-900/50 text-red-400 border border-red-800 px-3 py-1 rounded-full">
NÃO CONFIGURADA
</span>
)}
</div>
<p className="text-gray-400 text-sm">
Insira a API Key da Hostinger para gerenciar registros DNS e criar subdomínios automaticamente.
</p>
<div className="flex gap-2">
<input
type="password"
placeholder="Bearer token da Hostinger..."
value={apiKeyInput}
onChange={e => setApiKeyInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && saveKey()}
className="flex-1 bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 text-sm focus:outline-none focus:border-orange-500 placeholder-gray-600"
/>
<button
onClick={saveKey}
disabled={saving || !apiKeyInput.trim()}
className="bg-orange-500 hover:bg-orange-600 disabled:opacity-40 disabled:cursor-not-allowed text-white font-bold px-5 py-3 rounded-xl text-sm transition-colors"
>
{saving ? '...' : 'SALVAR'}
</button>
</div>
{success && <p className="text-green-400 text-sm font-medium">{success}</p>}
{error && <p className="text-red-400 text-sm font-medium">{error}</p>}
</div>
{/* Card Domínios */}
{hasKey && (
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="font-bold text-sm uppercase tracking-widest text-gray-400">Domínios / DNS</h2>
<button
onClick={fetchSubdomains}
disabled={loadingDomains}
className="bg-gray-800 hover:bg-gray-700 border border-gray-700 text-white font-bold px-4 py-2 rounded-xl text-xs transition-colors disabled:opacity-40"
>
{loadingDomains ? 'BUSCANDO...' : 'BUSCAR DA HOSTINGER'}
</button>
</div>
{subdomains.length === 0 && !loadingDomains && (
<p className="text-gray-600 text-sm text-center py-6">Clique em "Buscar da Hostinger" para listar os domínios.</p>
)}
{subdomains.length > 0 && (
<div className="space-y-2">
{subdomains.map((d: any, i: number) => (
<div key={i} className="flex items-center justify-between bg-gray-800 rounded-xl px-4 py-3">
<span className="font-mono text-sm text-white">{d.domain || d.name || JSON.stringify(d)}</span>
<span className="text-xs text-gray-500">{d.type || ''}</span>
</div>
))}
</div>
)}
</div>
)}
{/* Rodapé */}
<p className="text-center text-gray-700 text-xs">
subdominio.clube67.com acesso restrito VPN
</p>
</div>
</div>
);
}
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+6
View File
@@ -0,0 +1,6 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
createRoot(document.getElementById('root')!).render(<App />);
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
proxy: { '/api': 'http://localhost:8019' }
}
});