Initial commit: Setup GitOps structure with Kustomize
Some checks failed
CI/CD Pipeline / test (push) Successful in 28s
CI/CD Pipeline / build-and-push (push) Failing after 17s
CI/CD Pipeline / update-manifests (push) Has been skipped

- Added Kustomize base and overlays (dev, staging, prod)
- Added Gitea Actions CI/CD workflow
- Configured multi-environment deployment
This commit is contained in:
2026-01-14 16:46:10 +00:00
commit 77920f5a8a
26 changed files with 1390 additions and 0 deletions

22
.env.example Normal file
View File

@@ -0,0 +1,22 @@
# ===========================================
# Copie este arquivo para .env
# cp .env.example .env
# ===========================================
# Banco de Dados
DB_HOST=postgres
DB_PORT=5432
DB_NAME=inventory
DB_USER=postgres
DB_PASSWORD=postgres123
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
# Autenticação
JWT_SECRET=mude-isso-em-producao
AUTH_TOKEN=Tistech2018!#
# API
API_PORT=3001

112
.gitea/workflows/ci-cd.yaml Normal file
View File

@@ -0,0 +1,112 @@
name: CI/CD Pipeline
on:
push:
branches: [main, develop, staging]
pull_request:
branches: [main]
env:
REGISTRY: gitea.tisdev.cloud
IMAGE_PREFIX: gitea.tisdev.cloud/gitadmin/inventory-app
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Backend Dependencies
run: cd backend && npm install
- name: Install Frontend Dependencies
run: cd frontend && npm install
- name: Build Frontend
run: cd frontend && npm run build
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: gitadmin
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Get short SHA
id: sha
run: echo "short=$(echo ${{ gitea.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: Build and Push API
uses: docker/build-push-action@v5
with:
context: ./backend
push: true
tags: |
${{ env.IMAGE_PREFIX }}/api:${{ steps.sha.outputs.short }}
${{ env.IMAGE_PREFIX }}/api:${{ gitea.ref_name }}
${{ env.IMAGE_PREFIX }}/api:latest
- name: Build and Push Frontend
uses: docker/build-push-action@v5
with:
context: ./frontend
push: true
tags: |
${{ env.IMAGE_PREFIX }}/frontend:${{ steps.sha.outputs.short }}
${{ env.IMAGE_PREFIX }}/frontend:${{ gitea.ref_name }}
${{ env.IMAGE_PREFIX }}/frontend:latest
update-manifests:
needs: build-and-push
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
- name: Get short SHA
id: sha
run: echo "short=$(echo ${{ gitea.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: Determine environment
id: env
run: |
if [ "${{ gitea.ref_name }}" == "main" ]; then
echo "overlay=prod" >> $GITHUB_OUTPUT
elif [ "${{ gitea.ref_name }}" == "staging" ]; then
echo "overlay=staging" >> $GITHUB_OUTPUT
else
echo "overlay=dev" >> $GITHUB_OUTPUT
fi
- name: Update Kustomization
run: |
cd k8s/overlays/${{ steps.env.outputs.overlay }}
# Update image tags in kustomization.yaml
sed -i "s/newTag: .*/newTag: ${{ steps.sha.outputs.short }}/g" kustomization.yaml
- name: Commit and Push
run: |
git config user.name "Gitea Actions"
git config user.email "actions@gitea.tisdev.cloud"
git add -A
git diff --staged --quiet || git commit -m "Deploy: Update images to ${{ steps.sha.outputs.short }} [${{ steps.env.outputs.overlay }}]"
git push

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
.env
*.log
dist/
.DS_Store

7
backend/Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3001
CMD ["node", "server.js"]

21
backend/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "inventory-api",
"version": "1.0.0",
"description": "API para gerenciamento de inventário",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"helmet": "^7.1.0",
"redis": "^4.6.12"
},
"devDependencies": {
"nodemon": "^3.0.2"
}
}

302
backend/server.js Normal file
View File

@@ -0,0 +1,302 @@
const express = require('express');
const { Pool } = require('pg');
const cors = require('cors');
const helmet = require('helmet');
const crypto = require('crypto');
const app = express();
const PORT = process.env.PORT || 3001;
const AUTH_TOKEN = process.env.AUTH_TOKEN || 'Tistech2018!#';
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex');
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
// Simple JWT functions
const createToken = (payload) => {
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
const body = Buffer.from(JSON.stringify({ ...payload, exp: Date.now() + 24 * 60 * 60 * 1000 })).toString('base64url');
const signature = crypto.createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url');
return `${header}.${body}.${signature}`;
};
const verifyToken = (token) => {
try {
const [header, body, signature] = token.split('.');
const expectedSig = crypto.createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url');
if (signature !== expectedSig) return null;
const payload = JSON.parse(Buffer.from(body, 'base64url').toString());
if (payload.exp < Date.now()) return null;
return payload;
} catch { return null; }
};
// Auth middleware
const authMiddleware = (req, res, next) => {
if (req.path === '/api/auth/login' || req.path === '/health') return next();
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Token não fornecido' });
}
const token = authHeader.split(' ')[1];
const payload = verifyToken(token);
if (!payload) {
return res.status(401).json({ error: 'Token inválido ou expirado' });
}
req.user = payload;
next();
};
// Login endpoint
app.post('/api/auth/login', (req, res) => {
const { token } = req.body;
if (token === AUTH_TOKEN) {
const jwt = createToken({ authenticated: true, loginTime: Date.now() });
res.json({ success: true, token: jwt });
} else {
res.status(401).json({ error: 'Token inválido' });
}
});
// Verify token endpoint
app.get('/api/auth/verify', authMiddleware, (req, res) => {
res.json({ valid: true, user: req.user });
});
// Apply auth middleware to all /api routes except login
app.use('/api', authMiddleware);
// Database connection
const pool = new Pool({
host: process.env.DB_HOST || 'postgres-postgresql.inventory-app.svc.cluster.local',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'inventory',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'Postgres@2024!'
});
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date() });
});
// ============ ENVIRONMENTS ============
app.get('/api/environments', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM environments ORDER BY name');
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ============ SERVERS ============
app.get('/api/servers', async (req, res) => {
try {
const result = await pool.query(`
SELECT s.*, e.name as environment_name, e.color as environment_color
FROM servers s
LEFT JOIN environments e ON s.environment_id = e.id
ORDER BY s.name
`);
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.get('/api/servers/:id', async (req, res) => {
try {
const result = await pool.query(`
SELECT s.*, e.name as environment_name
FROM servers s
LEFT JOIN environments e ON s.environment_id = e.id
WHERE s.id = $1
`, [req.params.id]);
res.json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/servers', async (req, res) => {
const { name, hostname, ip_address, port, os, cpu_cores, ram_gb, disk_gb, environment_id, notes } = req.body;
try {
const result = await pool.query(`
INSERT INTO servers (name, hostname, ip_address, port, os, cpu_cores, ram_gb, disk_gb, environment_id, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *
`, [name, hostname, ip_address, port, os, cpu_cores, ram_gb, disk_gb, environment_id, notes]);
res.status(201).json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.put('/api/servers/:id', async (req, res) => {
const { name, hostname, ip_address, port, os, cpu_cores, ram_gb, disk_gb, environment_id, status, notes } = req.body;
try {
const result = await pool.query(`
UPDATE servers SET name=$1, hostname=$2, ip_address=$3, port=$4, os=$5, cpu_cores=$6, ram_gb=$7, disk_gb=$8, environment_id=$9, status=$10, notes=$11
WHERE id=$12 RETURNING *
`, [name, hostname, ip_address, port, os, cpu_cores, ram_gb, disk_gb, environment_id, status, notes, req.params.id]);
res.json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.delete('/api/servers/:id', async (req, res) => {
try {
await pool.query('DELETE FROM servers WHERE id = $1', [req.params.id]);
res.json({ message: 'Servidor deletado' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ============ APPLICATIONS ============
app.get('/api/applications', async (req, res) => {
try {
const result = await pool.query(`
SELECT a.*, e.name as environment_name, e.color as environment_color
FROM applications a
LEFT JOIN environments e ON a.environment_id = e.id
ORDER BY a.name
`);
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.get('/api/applications/:id', async (req, res) => {
try {
const result = await pool.query(`
SELECT a.*, e.name as environment_name
FROM applications a
LEFT JOIN environments e ON a.environment_id = e.id
WHERE a.id = $1
`, [req.params.id]);
res.json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/applications', async (req, res) => {
const { name, description, url, repository_url, version, environment_id, port } = req.body;
try {
const result = await pool.query(`
INSERT INTO applications (name, description, url, repository_url, version, environment_id, port)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *
`, [name, description, url, repository_url, version, environment_id, port]);
res.status(201).json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.delete('/api/applications/:id', async (req, res) => {
try {
await pool.query('DELETE FROM applications WHERE id = $1', [req.params.id]);
res.json({ message: 'Aplicação deletada' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ============ CREDENTIALS ============
app.get('/api/credentials', async (req, res) => {
try {
const result = await pool.query(`
SELECT c.*, s.name as server_name, s.ip_address as server_ip
FROM credentials c
LEFT JOIN servers s ON c.server_id = s.id
ORDER BY c.name
`);
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/credentials', async (req, res) => {
const { name, username, password, ssh_key, credential_type, server_id, notes } = req.body;
try {
const result = await pool.query(`
INSERT INTO credentials (name, username, password, ssh_key, credential_type, server_id, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *
`, [name, username, password, ssh_key, credential_type, server_id, notes]);
res.status(201).json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.delete('/api/credentials/:id', async (req, res) => {
try {
await pool.query('DELETE FROM credentials WHERE id = $1', [req.params.id]);
res.json({ message: 'Credencial deletada' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ============ APP CREDENTIALS ============
app.get('/api/app-credentials', async (req, res) => {
try {
const result = await pool.query(`
SELECT ac.*, a.name as application_name, a.url as application_url
FROM app_credentials ac
LEFT JOIN applications a ON ac.application_id = a.id
ORDER BY a.name, ac.name
`);
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/app-credentials', async (req, res) => {
const { application_id, name, username, password, notes } = req.body;
try {
const result = await pool.query(`
INSERT INTO app_credentials (application_id, name, username, password, notes)
VALUES ($1, $2, $3, $4, $5) RETURNING *
`, [application_id, name, username, password, notes]);
res.status(201).json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ============ DASHBOARD STATS ============
app.get('/api/dashboard/stats', async (req, res) => {
try {
const servers = await pool.query('SELECT COUNT(*) as count FROM servers');
const applications = await pool.query('SELECT COUNT(*) as count FROM applications');
const credentials = await pool.query('SELECT COUNT(*) as count FROM credentials');
const appCredentials = await pool.query('SELECT COUNT(*) as count FROM app_credentials');
const totalCpu = await pool.query('SELECT SUM(cpu_cores) as total FROM servers');
const totalRam = await pool.query('SELECT SUM(ram_gb) as total FROM servers');
const totalDisk = await pool.query('SELECT SUM(disk_gb) as total FROM servers');
res.json({
servers: parseInt(servers.rows[0].count),
applications: parseInt(applications.rows[0].count),
credentials: parseInt(credentials.rows[0].count) + parseInt(appCredentials.rows[0].count),
totalCpu: parseInt(totalCpu.rows[0].total) || 0,
totalRam: parseInt(totalRam.rows[0].total) || 0,
totalDisk: parseInt(totalDisk.rows[0].total) || 0
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(PORT, () => {
console.log(`API rodando na porta ${PORT}`);
});

74
docker-compose.yml Normal file
View File

@@ -0,0 +1,74 @@
version: '3.8'
# ===========================================
# AMBIENTE DE DESENVOLVIMENTO LOCAL
# Roda: docker compose up
# ===========================================
services:
# Frontend React (com hot reload)
frontend:
build: ./frontend
ports:
- "3000:80"
volumes:
- ./frontend/src:/app/src # Hot reload
depends_on:
- backend
environment:
- VITE_API_URL=http://localhost:3001
# Backend Node.js (com hot reload)
backend:
build: ./backend
ports:
- "3001:3001"
volumes:
- ./backend:/app
- /app/node_modules
depends_on:
- postgres
- redis
environment:
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=inventory
- DB_USER=postgres
- DB_PASSWORD=postgres123
- REDIS_HOST=redis
- REDIS_PORT=6379
- JWT_SECRET=dev-secret-change-in-production
- AUTH_TOKEN=Tistech2018!#
command: npm run dev # nodemon para hot reload
# PostgreSQL
postgres:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_DB=inventory
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres123
volumes:
- postgres_data:/var/lib/postgresql/data
# Redis (cache/sessões)
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
# Adminer (visualizar banco de dados)
adminer:
image: adminer
ports:
- "8080:8080"
depends_on:
- postgres
volumes:
postgres_data:
redis_data:

12
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM node:20-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Inventory Manager</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

17
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,17 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://inventory-api:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

27
frontend/package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "inventory-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"axios": "^1.6.2",
"lucide-react": "^0.294.0"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"vite": "^5.0.0"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

413
frontend/src/App.jsx Normal file
View File

@@ -0,0 +1,413 @@
import React, { useState, useEffect } from 'react'
import axios from 'axios'
import { Server, AppWindow, Key, Database, Cpu, HardDrive, MemoryStick, Eye, EyeOff, LogOut, Lock } from 'lucide-react'
const API_URL = '/api'
// Configure axios to use token
const api = axios.create({ baseURL: API_URL })
api.interceptors.request.use((config) => {
const token = localStorage.getItem('authToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('authToken')
window.location.reload()
}
return Promise.reject(error)
}
)
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [loginToken, setLoginToken] = useState('')
const [loginError, setLoginError] = useState('')
const [activeTab, setActiveTab] = useState('dashboard')
const [stats, setStats] = useState({})
const [servers, setServers] = useState([])
const [applications, setApplications] = useState([])
const [credentials, setCredentials] = useState([])
const [appCredentials, setAppCredentials] = useState([])
const [showPasswords, setShowPasswords] = useState({})
useEffect(() => {
checkAuth()
}, [])
const checkAuth = async () => {
const token = localStorage.getItem('authToken')
if (token) {
try {
await api.get('/auth/verify')
setIsAuthenticated(true)
fetchData()
} catch {
localStorage.removeItem('authToken')
}
}
setIsLoading(false)
}
const handleLogin = async (e) => {
e.preventDefault()
setLoginError('')
try {
const response = await axios.post(`${API_URL}/auth/login`, { token: loginToken })
localStorage.setItem('authToken', response.data.token)
setIsAuthenticated(true)
fetchData()
} catch (err) {
setLoginError('Token inválido')
}
}
const handleLogout = () => {
localStorage.removeItem('authToken')
setIsAuthenticated(false)
setLoginToken('')
}
const fetchData = async () => {
try {
const [statsRes, serversRes, appsRes, credsRes, appCredsRes] = await Promise.all([
api.get('/dashboard/stats'),
api.get('/servers'),
api.get('/applications'),
api.get('/credentials'),
api.get('/app-credentials')
])
setStats(statsRes.data)
setServers(serversRes.data)
setApplications(appsRes.data)
setCredentials(credsRes.data)
setAppCredentials(appCredsRes.data)
} catch (err) {
console.error('Erro ao carregar dados:', err)
}
}
const togglePassword = (id) => {
setShowPasswords(prev => ({ ...prev, [id]: !prev[id] }))
}
if (isLoading) {
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
)
}
if (!isAuthenticated) {
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
<div className="bg-gray-800 rounded-2xl shadow-2xl p-8 w-full max-w-md border border-gray-700">
<div className="flex flex-col items-center mb-8">
<div className="bg-blue-600 p-4 rounded-full mb-4">
<Lock className="w-8 h-8 text-white" />
</div>
<h1 className="text-2xl font-bold text-white">Inventory Manager</h1>
<p className="text-gray-400 mt-2">Digite o token de acesso</p>
</div>
<form onSubmit={handleLogin}>
<div className="mb-6">
<label className="block text-gray-300 text-sm font-medium mb-2">
Token de Acesso
</label>
<input
type="password"
value={loginToken}
onChange={(e) => setLoginToken(e.target.value)}
className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Digite seu token"
autoFocus
/>
</div>
{loginError && (
<div className="mb-4 p-3 bg-red-900/50 border border-red-700 rounded-lg text-red-300 text-sm">
{loginError}
</div>
)}
<button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-4 rounded-lg transition-colors"
>
Entrar
</button>
</form>
</div>
</div>
)
}
const StatCard = ({ icon: Icon, title, value, color }) => (
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<div className="flex items-center gap-4">
<div className={`p-3 rounded-lg ${color}`}>
<Icon className="w-6 h-6 text-white" />
</div>
<div>
<p className="text-gray-500 text-sm">{title}</p>
<p className="text-2xl font-bold text-gray-800">{value}</p>
</div>
</div>
</div>
)
const Dashboard = () => (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-gray-800">Dashboard</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard icon={Server} title="Servidores" value={stats.servers || 0} color="bg-blue-500" />
<StatCard icon={AppWindow} title="Aplicacoes" value={stats.applications || 0} color="bg-green-500" />
<StatCard icon={Key} title="Credenciais" value={stats.credentials || 0} color="bg-purple-500" />
<StatCard icon={Cpu} title="Total CPU" value={`${stats.totalCpu || 0} cores`} color="bg-orange-500" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<StatCard icon={MemoryStick} title="Total RAM" value={`${stats.totalRam || 0} GB`} color="bg-pink-500" />
<StatCard icon={HardDrive} title="Total Disco" value={`${stats.totalDisk || 0} GB`} color="bg-cyan-500" />
</div>
</div>
)
const ServersTab = () => (
<div className="space-y-4">
<h2 className="text-2xl font-bold text-gray-800">Servidores</h2>
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">IP</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">OS</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">CPU</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">RAM</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{servers.map(server => (
<tr key={server.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<div className="font-medium text-gray-900">{server.name}</div>
<div className="text-sm text-gray-500">{server.hostname}</div>
</td>
<td className="px-6 py-4 text-gray-600">{server.ip_address}</td>
<td className="px-6 py-4 text-gray-600 text-sm">{server.os}</td>
<td className="px-6 py-4 text-gray-600">{server.cpu_cores} cores</td>
<td className="px-6 py-4 text-gray-600">{server.ram_gb} GB</td>
<td className="px-6 py-4">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
server.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{server.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
const ApplicationsTab = () => (
<div className="space-y-4">
<h2 className="text-2xl font-bold text-gray-800">Aplicacoes</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{applications.map(app => (
<div key={app.id} className="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<div className="flex justify-between items-start mb-4">
<h3 className="font-semibold text-lg text-gray-800">{app.name}</h3>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
app.status === 'running' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}`}>
{app.status}
</span>
</div>
<p className="text-gray-600 text-sm mb-4">{app.description}</p>
{app.url && (
<a
href={app.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 text-sm"
>
Acessar
</a>
)}
{app.port && (
<p className="text-gray-500 text-sm mt-2">Porta: {app.port}</p>
)}
</div>
))}
</div>
</div>
)
const CredentialsTab = () => (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-gray-800">Credenciais</h2>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-700">Credenciais de Servidores</h3>
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Servidor</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Usuario</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Senha</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{credentials.map(cred => (
<tr key={cred.id} className="hover:bg-gray-50">
<td className="px-6 py-4 font-medium text-gray-900">{cred.name}</td>
<td className="px-6 py-4 text-gray-600">
{cred.server_name} ({cred.server_ip})
</td>
<td className="px-6 py-4 text-gray-600">{cred.username}</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<code className="bg-gray-100 px-2 py-1 rounded text-sm">
{showPasswords[cred.id] ? cred.password : '**********'}
</code>
<button
onClick={() => togglePassword(cred.id)}
className="text-gray-500 hover:text-gray-700"
>
{showPasswords[cred.id] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-700">Credenciais de Aplicacoes</h3>
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Aplicacao</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Usuario</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Senha</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">URL</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{appCredentials.map(cred => (
<tr key={cred.id} className="hover:bg-gray-50">
<td className="px-6 py-4 font-medium text-gray-900">{cred.application_name}</td>
<td className="px-6 py-4 text-gray-600">{cred.name}</td>
<td className="px-6 py-4 text-gray-600">{cred.username}</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<code className="bg-gray-100 px-2 py-1 rounded text-sm">
{showPasswords[`app-${cred.id}`] ? cred.password : '**********'}
</code>
<button
onClick={() => togglePassword(`app-${cred.id}`)}
className="text-gray-500 hover:text-gray-700"
>
{showPasswords[`app-${cred.id}`] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</td>
<td className="px-6 py-4">
{cred.application_url && (
<a
href={cred.application_url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800"
>
Abrir
</a>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
const tabs = [
{ id: 'dashboard', label: 'Dashboard', icon: Database },
{ id: 'servers', label: 'Servidores', icon: Server },
{ id: 'applications', label: 'Aplicacoes', icon: AppWindow },
{ id: 'credentials', label: 'Credenciais', icon: Key },
]
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<Database className="w-8 h-8 text-blue-600" />
<span className="ml-2 text-xl font-bold text-gray-800">Inventory Manager</span>
</div>
<div className="flex items-center">
<button
onClick={handleLogout}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-red-600 transition-colors"
>
<LogOut className="w-5 h-5" />
Sair
</button>
</div>
</div>
</div>
</nav>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex gap-4 mb-8 border-b border-gray-200">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 -mb-px border-b-2 transition-colors ${
activeTab === tab.id
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
<tab.icon className="w-5 h-5" />
{tab.label}
</button>
))}
</div>
{activeTab === 'dashboard' && <Dashboard />}
{activeTab === 'servers' && <ServersTab />}
{activeTab === 'applications' && <ApplicationsTab />}
{activeTab === 'credentials' && <CredentialsTab />}
</div>
</div>
)
}
export default App

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

@@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: 'Inter', system-ui, sans-serif;
}

10
frontend/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@@ -0,0 +1,7 @@
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
}

10
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173
}
})

View File

@@ -0,0 +1,57 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: inventory-api
spec:
replicas: 1
selector:
matchLabels:
app: inventory-api
template:
metadata:
labels:
app: inventory-api
spec:
containers:
- name: inventory-api
image: gitea.tisdev.cloud/gitadmin/inventory-app/api:latest
ports:
- containerPort: 3001
env:
- name: DB_HOST
value: "postgres-postgresql"
- name: DB_PORT
value: "5432"
- name: DB_NAME
value: "inventory"
- name: DB_USER
value: "postgres"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-password
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: inventory-secrets
key: jwt-secret
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: inventory-api
spec:
selector:
app: inventory-api
ports:
- port: 3001
targetPort: 3001
type: ClusterIP

View File

@@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: inventory-frontend
spec:
replicas: 1
selector:
matchLabels:
app: inventory-frontend
template:
metadata:
labels:
app: inventory-frontend
spec:
containers:
- name: inventory-frontend
image: gitea.tisdev.cloud/gitadmin/inventory-app/frontend:latest
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
name: inventory-frontend
spec:
selector:
app: inventory-frontend
ports:
- port: 80
targetPort: 80
type: ClusterIP

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment-api.yaml
- deployment-frontend.yaml
- secrets.yaml

15
k8s/base/secrets.yaml Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
type: Opaque
stringData:
postgres-password: postgres123
---
apiVersion: v1
kind: Secret
metadata:
name: inventory-secrets
type: Opaque
stringData:
jwt-secret: super-secret-jwt-key-change-in-production

View File

@@ -0,0 +1,22 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: inventory-dev
resources:
- ../../base
replicas:
- name: inventory-api
count: 1
- name: inventory-frontend
count: 1
images:
- name: gitea.tisdev.cloud/gitadmin/inventory-app/api
newTag: latest
- name: gitea.tisdev.cloud/gitadmin/inventory-app/frontend
newTag: latest
commonLabels:
environment: dev

View File

@@ -0,0 +1,31 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: inventory-app
resources:
- ../../base
replicas:
- name: inventory-api
count: 2
- name: inventory-frontend
count: 2
images:
- name: gitea.tisdev.cloud/gitadmin/inventory-app/api
newTag: latest
- name: gitea.tisdev.cloud/gitadmin/inventory-app/frontend
newTag: latest
commonLabels:
environment: production
patches:
- patch: |-
- op: replace
path: /stringData/jwt-secret
value: prod-super-secret-jwt-key-2024
target:
kind: Secret
name: inventory-secrets

View File

@@ -0,0 +1,22 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: inventory-staging
resources:
- ../../base
replicas:
- name: inventory-api
count: 1
- name: inventory-frontend
count: 1
images:
- name: gitea.tisdev.cloud/gitadmin/inventory-app/api
newTag: latest
- name: gitea.tisdev.cloud/gitadmin/inventory-app/frontend
newTag: latest
commonLabels:
environment: staging

56
scripts/deploy.sh Normal file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
# ===========================================
# Script para deploy no Kubernetes
# Execute: ./scripts/deploy.sh [dev|staging|prod]
# ===========================================
ENV=${1:-prod}
echo "🚀 Fazendo deploy no ambiente: $ENV"
# Cores
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
# Verificar se está no diretório correto
if [ ! -f "docker-compose.yml" ]; then
echo -e "${RED}Erro: Execute este script na raiz do projeto${NC}"
exit 1
fi
# Build das imagens
echo -e "\n[1/4] Construindo imagens Docker..."
docker build -t inventory-api:latest ./backend
docker build -t inventory-frontend:latest ./frontend
# Tag com versão
VERSION=$(date +%Y%m%d-%H%M%S)
docker tag inventory-api:latest inventory-api:$VERSION
docker tag inventory-frontend:latest inventory-frontend:$VERSION
echo -e "${GREEN}✓ Imagens construídas: $VERSION${NC}"
# Commit e push
echo -e "\n[2/4] Enviando código para GitHub..."
git add -A
git commit -m "Deploy $ENV - $VERSION" || true
git push origin main
echo -e "${GREEN}✓ Código enviado${NC}"
# ArgoCD vai sincronizar automaticamente
echo -e "\n[3/4] ArgoCD sincronizando..."
echo "Aguarde o ArgoCD detectar as mudanças (até 3 minutos)"
echo "Ou force a sincronização em: https://argocd.dev-ai.cloud"
echo -e "\n${GREEN}========================================${NC}"
echo -e "${GREEN}✅ Deploy iniciado com sucesso!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo "Acompanhe em:"
echo " ArgoCD: https://argocd.dev-ai.cloud"
echo " Portainer: https://kube.dev-ai.cloud"
echo " App: https://inv.dev-ai.cloud"
echo ""

77
scripts/setup-local.sh Normal file
View File

@@ -0,0 +1,77 @@
#!/bin/bash
# ===========================================
# Script para configurar ambiente local
# Execute: chmod +x scripts/setup-local.sh && ./scripts/setup-local.sh
# ===========================================
echo "🚀 Configurando ambiente de desenvolvimento local..."
# Cores
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 1. Verificar Docker
echo -e "\n${YELLOW}[1/5] Verificando Docker...${NC}"
if command -v docker &> /dev/null; then
echo -e "${GREEN}✓ Docker instalado${NC}"
else
echo "Docker não encontrado. Instalando..."
sudo apt update && sudo apt install -y docker.io docker-compose-v2
sudo usermod -aG docker $USER
echo "⚠️ Faça logout e login novamente para usar Docker sem sudo"
fi
# 2. Verificar Node.js
echo -e "\n${YELLOW}[2/5] Verificando Node.js...${NC}"
if command -v node &> /dev/null; then
echo -e "${GREEN}✓ Node.js $(node -v) instalado${NC}"
else
echo "Node.js não encontrado. Instalando..."
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
fi
# 3. Verificar Git
echo -e "\n${YELLOW}[3/5] Verificando Git...${NC}"
if command -v git &> /dev/null; then
echo -e "${GREEN}✓ Git instalado${NC}"
else
echo "Git não encontrado. Instalando..."
sudo apt install -y git
fi
# 4. Criar arquivo .env
echo -e "\n${YELLOW}[4/5] Criando arquivo .env...${NC}"
if [ ! -f .env ]; then
cp .env.example .env
echo -e "${GREEN}✓ Arquivo .env criado${NC}"
else
echo -e "${GREEN}✓ Arquivo .env já existe${NC}"
fi
# 5. Instalar dependências
echo -e "\n${YELLOW}[5/5] Instalando dependências...${NC}"
cd backend && npm install && cd ..
cd frontend && npm install && cd ..
echo -e "${GREEN}✓ Dependências instaladas${NC}"
echo -e "\n${GREEN}========================================${NC}"
echo -e "${GREEN}✅ Ambiente configurado com sucesso!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo "Para iniciar o desenvolvimento:"
echo ""
echo " docker compose up # Inicia todos os serviços"
echo ""
echo "Ou sem Docker:"
echo ""
echo " cd backend && npm run dev # Terminal 1"
echo " cd frontend && npm run dev # Terminal 2"
echo ""
echo "URLs:"
echo " Frontend: http://localhost:3000"
echo " Backend: http://localhost:3001"
echo " Adminer: http://localhost:8080"
echo ""