Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 50 additions & 38 deletions DOMAIN.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,75 @@
# Configuração de Domínio Próprio

> **Nota:** Este é um guia de referência para ser usado **após o deploy da VM na Oracle Cloud** (ver `terraform/README.md` e PRD §14.7). As instruções abaixo pressupõem que a instância OCI já está provisionada e a aplicação está rodando.
> **Status:** ✅ Configurado e funcionando — 19/Jun/2026

Para habilitar a presença pública do **Simples Editor** em um domínio próprio (ex: `simples.seu-dominio.edu.br`), siga as instruções abaixo.
## Domínio

## Pré-requisitos
| Item | Valor |
|---|---|
| **Domínio** | `simples.163.176.220.47.nip.io` |
| **IP OCI** | `163.176.220.47` |
| **HTTPS** | Let's Encrypt (certbot) |
| **Expira cert** | 16/Set/2026 (renovação automática) |
| **Email** | carlos.barbosa@alunos.ifsuldeminas.edu.br |

- [ ] VM OCI Ampere A1 provisionada (via Terraform em `terraform/`)
- [ ] Aplicação rodando na VM (`docker compose up -d`)
- [ ] Domínio registrado com acesso ao painel DNS
## URLs de acesso

## 1. Apontamento de DNS
| URL | Descrição |
|---|---|
| `https://simples.163.176.220.47.nip.io` | IDE completa |
| `https://simples.163.176.220.47.nip.io/login` | Login Supabase |
| `https://simples.163.176.220.47.nip.io/api/health` | Health check |

Acesse o painel do provedor de DNS do seu domínio e crie um registro do tipo **A**:
- **Nome/Host**: `simples` (ou o subdomínio desejado)
- **Tipo**: `A`
- **Valor**: `<IP_PUBLICO_DA_OCI>` (substitua pelo IP da sua instância na Oracle Cloud)
- **TTL**: `Auto` ou `3600`
> HTTP (porta 80) redireciona automaticamente para HTTPS.

Aguarde a propagação do DNS (pode levar alguns minutos a algumas horas). Verifique se o domínio já responde ao IP usando ferramentas como `ping` ou `nslookup`.
## Como foi configurado

## 2. Configuração do Nginx e Let's Encrypt (TLS)

Após a propagação, execute os comandos abaixo na instância OCI. Como alternativa, use o script `setup-tls.sh` provisionado pelo cloud-init (`/home/ubuntu/setup-tls.sh`):
### 1. DNS (nip.io)
`nip.io` é um serviço gratuito de DNS wildcard — qualquer subdomínio no formato `<nome>.<IP>.nip.io` resolve automaticamente para o IP. Zero configuração de DNS necessária.

Verificação:
```bash
# Opção 1: Script automatizado (recomendado)
sudo DOMAIN=simples.seu-dominio.edu.br EMAIL=admin@seu-dominio.edu.br /home/ubuntu/setup-tls.sh
nslookup simples.163.176.220.47.nip.io
# → 163.176.220.47
```

# Opção 2: Passo a passo manual
### 2. OCI Security List
No OCI Console, adicionar regras de ingresso na subnet pública:
- TCP/80 (HTTP) de 0.0.0.0/0
- TCP/443 (HTTPS) de 0.0.0.0/0

### 3. Let's Encrypt (certbot)
```bash
# Instalar certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
# Instalar
sudo apt-get install -y certbot

# Parar o nginx temporariamente
# Parar nginx temporariamente
docker compose stop nginx

# Emitir o certificado TLS para o domínio configurado
sudo certbot certonly --standalone -d simples.seu-dominio.edu.br \
--non-interactive --agree-tos -m admin@seu-dominio.edu.br
# Obter certificado
sudo certbot certonly --standalone \
-d simples.163.176.220.47.nip.io \
--non-interactive --agree-tos \
-m carlos.barbosa@alunos.ifsuldeminas.edu.br

# Mover os certificados para a pasta do Nginx
sudo cp /etc/letsencrypt/live/simples.seu-dominio.edu.br/fullchain.pem ./nginx/certs/
sudo cp /etc/letsencrypt/live/simples.seu-dominio.edu.br/privkey.pem ./nginx/certs/
# Copiar para o diretório do nginx
sudo cp /etc/letsencrypt/live/simples.163.176.220.47.nip.io/fullchain.pem ./nginx/certs/
sudo cp /etc/letsencrypt/live/simples.163.176.220.47.nip.io/privkey.pem ./nginx/certs/

# Reiniciar o Nginx
docker compose start nginx
# Reiniciar
docker compose up -d nginx
```

## 3. Renovação Automática

Os certificados do Let's Encrypt expiram a cada 90 dias. Adicione no cronjob para renovação automática:

### 4. Renovação automática
```bash
echo "0 3 * * * certbot renew --quiet --post-hook 'cd /home/ubuntu/simples-online && docker compose restart nginx'" | sudo crontab -
echo "0 3 * * * certbot renew --quiet --post-hook 'docker compose -f /home/ubuntu/simples-online/docker-compose.yml restart nginx'" | sudo crontab -
```

## Nginx config (resumo)
- Porta 80: redireciona para HTTPS
- Porta 443: SSL com proxy para frontend:80 e backend:5000
- Certificados montados via volume: `./nginx/certs:/etc/nginx/certs:ro`

## Validação (Critérios de Aceite)
- [ ] Domínio próprio apontando para o IP da OCI (requer deploy prévio da VM).
- [ ] Domínio resolvendo e acessível publicamente via HTTPS (requer execução do setup-tls.sh).
- [x] Domínio próprio apontando para o IP da OCI.
- [x] Domínio resolvendo e acessível publicamente via HTTPS.
63 changes: 63 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
SIMPLES_EDITOR_OPTIONS,
} from "./lib/simples-language";
import type * as Monaco from "monaco-editor";
import { supabase } from "./lib/supabase";
import { LoginPage } from "./components/LoginPage";

// ── Types ────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -135,6 +137,28 @@ function App() {
const [wsConnected, setWsConnected] = useState(false);
const [examplesOpen, setExamplesOpen] = useState(false);

// ── Auth State ──────────────────────────────────────────────────────────

const [authChecked, setAuthChecked] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(
import.meta.env.VITE_DEMO_MODE === "true"
);

useEffect(() => {
if (import.meta.env.VITE_DEMO_MODE === "true") {
setAuthChecked(true);
return;
}
supabase.auth.getSession().then(({ data }) => {
setIsAuthenticated(!!data.session);
setAuthChecked(true);
});
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setIsAuthenticated(!!session);
});
return () => subscription.unsubscribe();
}, []);

// ── Monaco Language Registration ───────────────────────────────────────

const handleBeforeMount: BeforeMount = useCallback((monaco) => {
Expand Down Expand Up @@ -418,6 +442,11 @@ function App() {
setExamplesOpen(false);
}, []);

const handleLogout = useCallback(async () => {
await supabase.auth.signOut();
setIsAuthenticated(false);
}, []);

// Close examples dropdown on outside click
useEffect(() => {
const handler = (e: MouseEvent) => {
Expand All @@ -442,6 +471,31 @@ function App() {

// ── Render ──────────────────────────────────────────────────────────────

// Auth gate: show login if not authenticated (skip in demo mode)
if (!authChecked) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-950">
<div className="text-cyan-400 text-lg animate-pulse">Carregando...</div>
</div>
);
}

if (!isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-cyan-400">Simples Editor</h1>
<p className="text-gray-400 mt-2">
IDE web para a linguagem SIMPLES
</p>
</div>
<LoginPage />
</div>
</div>
);
}

return (
<div className="flex flex-col h-screen bg-gray-950 text-gray-100">
{/* Header */}
Expand Down Expand Up @@ -502,6 +556,15 @@ function App() {
>
Limpar
</button>

{import.meta.env.VITE_DEMO_MODE !== "true" && (
<button
onClick={handleLogout}
className="text-sm text-gray-400 hover:text-white transition-colors"
>
Sair
</button>
)}
</div>
</header>

Expand Down
25 changes: 25 additions & 0 deletions nginx/default.conf
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
# HTTP → HTTPS redirect
server {
listen 80;
server_name localhost;

# Let's Encrypt webroot challenges (certbot renew)
location /.well-known/acme-challenge/ {
root /tmp;
}

location / {
return 301 https://$host$request_uri;
}
}

# HTTPS
server {
listen 443 ssl;
server_name localhost;

ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;

location / {
proxy_pass http://frontend:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /api/ {
proxy_pass http://backend:5000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 30s;
}

Expand All @@ -23,6 +47,7 @@ server {
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
Expand Down
Loading