998 lines
26 KiB
Markdown
998 lines
26 KiB
Markdown
---
|
|
title: Gpt 2
|
|
description:
|
|
published: true
|
|
date: 2026-07-24T19:37:15.466Z
|
|
tags:
|
|
editor: markdown
|
|
dateCreated: 2026-07-24T19:37:15.466Z
|
|
---
|
|
|
|
# Guia de Estudo: Meu GPT v2 em LXC no Proxmox
|
|
|
|
Este documento descreve como evoluir o projeto original para uma aplicacao web multiusuario com:
|
|
|
|
- cadastro e login;
|
|
- usuarios salvos em banco de dados;
|
|
- historico de conversas;
|
|
- upload de arquivos;
|
|
- controle de consumo e custo por usuario;
|
|
- painel administrativo;
|
|
- hospedagem em um container LXC no Proxmox.
|
|
|
|
> Importante: a aplicacao usa a **OpenAI API**, cuja cobranca e separada da assinatura do ChatGPT. A chave da API pertence ao administrador da aplicacao e deve permanecer somente no servidor.
|
|
|
|
## 1. Resultado esperado
|
|
|
|
O usuario acessa a aplicacao pelo navegador, cria uma conta ou entra com seu login e conversa com o GPT. As conversas ficam salvas e podem ser abertas novamente. O usuario pode anexar arquivos e consultar seu consumo.
|
|
|
|
O administrador pode:
|
|
|
|
- ativar e desativar usuarios;
|
|
- definir limite mensal individual;
|
|
- consultar consumo por usuario;
|
|
- consultar quantidade de tokens e custo estimado;
|
|
- impedir novas chamadas quando o limite for atingido;
|
|
- escolher quais modelos ficam disponiveis.
|
|
|
|
## 2. Arquitetura proposta
|
|
|
|
```text
|
|
Navegador
|
|
|
|
|
| HTTPS
|
|
v
|
|
Nginx no LXC
|
|
|
|
|
v
|
|
Next.js
|
|
|-- telas React
|
|
|-- login e sessoes
|
|
|-- API de chat
|
|
|-- API de upload
|
|
|-- regras de limite
|
|
|
|
|
|----------> PostgreSQL
|
|
| |-- usuarios
|
|
| |-- conversas
|
|
| |-- mensagens
|
|
| |-- anexos
|
|
| |-- consumo
|
|
|
|
|
|----------> armazenamento privado no LXC
|
|
| |-- arquivos enviados
|
|
|
|
|
`----------> OpenAI Responses API
|
|
|-- respostas do modelo
|
|
|-- leitura de arquivos compativeis
|
|
`-- contagem de tokens
|
|
```
|
|
|
|
## 3. Tecnologias escolhidas e por que usa-las
|
|
|
|
### Next.js e React
|
|
|
|
O Next.js continua sendo a aplicacao principal. Ele permite criar as telas e as rotas de backend no mesmo projeto.
|
|
|
|
O React cuida das partes interativas da tela:
|
|
|
|
- lista de conversas;
|
|
- envio de mensagens;
|
|
- indicador de resposta;
|
|
- selecao e upload de arquivos;
|
|
- tela de consumo.
|
|
|
|
### TypeScript
|
|
|
|
O TypeScript reduz erros ao definir claramente o formato de usuarios, mensagens, anexos e registros de consumo.
|
|
|
|
Exemplo:
|
|
|
|
```ts
|
|
type Message = {
|
|
id: string;
|
|
role: "USER" | "ASSISTANT";
|
|
content: string;
|
|
};
|
|
```
|
|
|
|
### PostgreSQL
|
|
|
|
O PostgreSQL sera o banco de dados. Ele e adequado para dados relacionados, por exemplo:
|
|
|
|
- um usuario possui varias conversas;
|
|
- uma conversa possui varias mensagens;
|
|
- uma mensagem pode possuir varios anexos;
|
|
- cada chamada da API gera um registro de consumo.
|
|
|
|
Para estudo e para uma instalacao pequena, o PostgreSQL pode rodar no mesmo LXC. Em um ambiente maior, pode ser movido para outro container.
|
|
|
|
### Prisma ORM
|
|
|
|
O Prisma faz a comunicacao entre o TypeScript e o PostgreSQL.
|
|
|
|
Em vez de escrever SQL para cada operacao, o projeto pode usar:
|
|
|
|
```ts
|
|
const conversations = await prisma.conversation.findMany({
|
|
where: { userId },
|
|
orderBy: { updatedAt: "desc" },
|
|
});
|
|
```
|
|
|
|
As migrations do Prisma tambem registram as alteracoes feitas na estrutura do banco.
|
|
|
|
### Auth.js
|
|
|
|
O Auth.js sera responsavel pelo fluxo de autenticacao e pela sessao do usuario. Neste projeto:
|
|
|
|
- o usuario informa e-mail e senha;
|
|
- a aplicacao procura o usuario no PostgreSQL;
|
|
- a senha e comparada com o hash salvo;
|
|
- uma sessao assinada identifica o usuario nas proximas requisicoes.
|
|
|
|
A senha nunca deve ser salva em texto puro. Use `bcryptjs` ou `argon2` para gerar o hash.
|
|
|
|
### Armazenamento de arquivos
|
|
|
|
Na primeira versao, os arquivos podem ser armazenados em uma pasta privada do LXC:
|
|
|
|
```text
|
|
/var/lib/meu-gpt/uploads
|
|
```
|
|
|
|
Essa pasta nao deve ficar dentro de `public/`, pois isso permitiria tentar acessar o arquivo diretamente por URL.
|
|
|
|
Os metadados ficam no PostgreSQL, mas o conteudo fica no disco. Mais tarde, o armazenamento pode ser trocado por MinIO ou outro servico compativel com S3.
|
|
|
|
### Nginx, PM2 e HTTPS
|
|
|
|
- **Nginx** recebe os acessos HTTPS e encaminha para o Next.js.
|
|
- **PM2** mantem o processo do Next.js em execucao.
|
|
- **Certbot e Let's Encrypt** fornecem o certificado HTTPS.
|
|
|
|
## 4. Estrutura sugerida do projeto
|
|
|
|
```text
|
|
meu-gpt/
|
|
|-- app/
|
|
| |-- (auth)/
|
|
| | |-- login/page.tsx
|
|
| | `-- cadastro/page.tsx
|
|
| |-- (app)/
|
|
| | |-- chat/[conversationId]/page.tsx
|
|
| | |-- configuracoes/page.tsx
|
|
| | `-- consumo/page.tsx
|
|
| |-- admin/
|
|
| | |-- usuarios/page.tsx
|
|
| | `-- consumo/page.tsx
|
|
| `-- api/
|
|
| |-- auth/[...nextauth]/route.ts
|
|
| |-- register/route.ts
|
|
| |-- conversations/route.ts
|
|
| |-- conversations/[id]/messages/route.ts
|
|
| |-- upload/route.ts
|
|
| `-- admin/users/[id]/route.ts
|
|
|-- components/
|
|
|-- lib/
|
|
| |-- auth.ts
|
|
| |-- prisma.ts
|
|
| |-- openai.ts
|
|
| |-- permissions.ts
|
|
| |-- pricing.ts
|
|
| `-- storage.ts
|
|
|-- prisma/
|
|
| `-- schema.prisma
|
|
|-- private-uploads/
|
|
|-- middleware.ts
|
|
|-- .env
|
|
`-- package.json
|
|
```
|
|
|
|
Em producao, use `/var/lib/meu-gpt/uploads` em vez de `private-uploads/`.
|
|
|
|
## 5. Modelo de dados
|
|
|
|
O modelo abaixo e um ponto de partida. Ele pode ser ajustado durante a implementacao.
|
|
|
|
```prisma
|
|
enum UserRole {
|
|
USER
|
|
ADMIN
|
|
}
|
|
|
|
enum UserStatus {
|
|
ACTIVE
|
|
BLOCKED
|
|
}
|
|
|
|
enum MessageRole {
|
|
USER
|
|
ASSISTANT
|
|
SYSTEM
|
|
}
|
|
|
|
enum UsageStatus {
|
|
SUCCESS
|
|
ERROR
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
name String
|
|
email String @unique
|
|
passwordHash String
|
|
role UserRole @default(USER)
|
|
status UserStatus @default(ACTIVE)
|
|
monthlyLimitUsd Decimal @default(5.00) @db.Decimal(12, 4)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
conversations Conversation[]
|
|
usageRecords UsageRecord[]
|
|
attachments Attachment[]
|
|
}
|
|
|
|
model Conversation {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
title String @default("Nova conversa")
|
|
model String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
archivedAt DateTime?
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
messages Message[]
|
|
usageRecords UsageRecord[]
|
|
|
|
@@index([userId, updatedAt])
|
|
}
|
|
|
|
model Message {
|
|
id String @id @default(cuid())
|
|
conversationId String
|
|
role MessageRole
|
|
content String @db.Text
|
|
openaiResponseId String?
|
|
createdAt DateTime @default(now())
|
|
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
attachments Attachment[]
|
|
|
|
@@index([conversationId, createdAt])
|
|
}
|
|
|
|
model Attachment {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
messageId String?
|
|
originalName String
|
|
storedName String
|
|
mimeType String
|
|
sizeBytes Int
|
|
sha256 String
|
|
openaiFileId String?
|
|
createdAt DateTime @default(now())
|
|
deletedAt DateTime?
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
message Message? @relation(fields: [messageId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId, createdAt])
|
|
}
|
|
|
|
model UsageRecord {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
conversationId String?
|
|
model String
|
|
inputTokens Int @default(0)
|
|
cachedInputTokens Int @default(0)
|
|
outputTokens Int @default(0)
|
|
reasoningTokens Int @default(0)
|
|
totalTokens Int @default(0)
|
|
costUsd Decimal @default(0) @db.Decimal(18, 8)
|
|
status UsageStatus
|
|
requestId String?
|
|
createdAt DateTime @default(now())
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
conversation Conversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId, createdAt])
|
|
}
|
|
```
|
|
|
|
## 6. Instalacao no LXC
|
|
|
|
### Recursos recomendados
|
|
|
|
Para poucos usuarios:
|
|
|
|
```text
|
|
Sistema: Debian 12 ou Ubuntu 24.04
|
|
CPU: 2 a 4 cores
|
|
RAM: 4 GB
|
|
Disco: 30 GB ou mais
|
|
Container nao privilegiado: sim
|
|
Inicializar no boot: sim
|
|
```
|
|
|
|
O tamanho do disco depende principalmente dos uploads e dos backups.
|
|
|
|
### Instalar os pacotes
|
|
|
|
```bash
|
|
apt update
|
|
apt upgrade -y
|
|
apt install -y curl git nginx postgresql postgresql-contrib ca-certificates
|
|
```
|
|
|
|
Instale uma versao LTS do Node.js compativel com as versoes atuais do Next.js e do Prisma escolhidas no momento da instalacao.
|
|
|
|
Confirme:
|
|
|
|
```bash
|
|
node -v
|
|
npm -v
|
|
psql --version
|
|
```
|
|
|
|
### Criar banco e usuario
|
|
|
|
Entre no PostgreSQL:
|
|
|
|
```bash
|
|
sudo -u postgres psql
|
|
```
|
|
|
|
Execute, substituindo a senha:
|
|
|
|
```sql
|
|
CREATE USER meu_gpt_user WITH ENCRYPTED PASSWORD 'COLOQUE_UMA_SENHA_FORTE';
|
|
CREATE DATABASE meu_gpt OWNER meu_gpt_user;
|
|
\q
|
|
```
|
|
|
|
O PostgreSQL pode continuar aceitando conexoes somente do proprio LXC.
|
|
|
|
### Instalar dependencias do projeto
|
|
|
|
Dentro da pasta do projeto:
|
|
|
|
```bash
|
|
npm install openai next-auth bcryptjs zod
|
|
npm install prisma @prisma/client
|
|
npm install -D @types/bcryptjs
|
|
```
|
|
|
|
Bibliotecas opcionais:
|
|
|
|
```bash
|
|
npm install file-type
|
|
```
|
|
|
|
`zod` valida os dados recebidos. `file-type` ajuda a conferir o tipo real do arquivo, em vez de confiar somente no nome informado pelo navegador.
|
|
|
|
## 7. Variaveis de ambiente
|
|
|
|
Exemplo de `.env`:
|
|
|
|
```env
|
|
DATABASE_URL="postgresql://meu_gpt_user:SENHA_FORTE@127.0.0.1:5432/meu_gpt"
|
|
|
|
AUTH_SECRET="GERE_UMA_CHAVE_LONGA_E_ALEATORIA"
|
|
AUTH_URL="https://gpt.seudominio.com"
|
|
|
|
OPENAI_API_KEY="SUA_CHAVE_DA_API"
|
|
OPENAI_MODEL="MODELO_PERMITIDO_PELA_SUA_CONTA"
|
|
|
|
UPLOAD_DIR="/var/lib/meu-gpt/uploads"
|
|
MAX_UPLOAD_BYTES="10485760"
|
|
|
|
DEFAULT_MONTHLY_LIMIT_USD="5.00"
|
|
```
|
|
|
|
Gere o segredo da autenticacao com:
|
|
|
|
```bash
|
|
openssl rand -base64 32
|
|
```
|
|
|
|
Regras:
|
|
|
|
- nao envie `.env` para Git;
|
|
- nunca use variavel iniciada por `NEXT_PUBLIC_` para a chave da OpenAI;
|
|
- deixe o arquivo legivel apenas pelo usuario que executa a aplicacao;
|
|
- troque imediatamente qualquer segredo que tenha sido exposto.
|
|
|
|
## 8. Cadastro e login
|
|
|
|
### Cadastro
|
|
|
|
O endpoint de cadastro deve:
|
|
|
|
1. validar nome, e-mail e senha com `zod`;
|
|
2. normalizar o e-mail para letras minusculas;
|
|
3. exigir uma senha com tamanho minimo;
|
|
4. verificar se o e-mail ja existe;
|
|
5. gerar o hash da senha;
|
|
6. criar o usuario com limite mensal padrao;
|
|
7. nunca devolver `passwordHash` na resposta.
|
|
|
|
Exemplo do hash:
|
|
|
|
```ts
|
|
import bcrypt from "bcryptjs";
|
|
|
|
const passwordHash = await bcrypt.hash(password, 12);
|
|
```
|
|
|
|
### Login
|
|
|
|
No login:
|
|
|
|
1. procure o usuario pelo e-mail;
|
|
2. verifique se ele esta ativo;
|
|
3. compare a senha informada com o hash;
|
|
4. crie uma sessao contendo somente os identificadores necessarios;
|
|
5. redirecione para a tela de chat.
|
|
|
|
```ts
|
|
const validPassword = await bcrypt.compare(password, user.passwordHash);
|
|
```
|
|
|
|
Proteja todas as rotas de chat, upload, historico e consumo. O backend deve obter o `userId` da sessao; nunca aceite um `userId` enviado pelo navegador como prova de identidade.
|
|
|
|
### Cadastro publico ou controlado
|
|
|
|
Para uma aplicacao pessoal ou empresarial, e mais seguro escolher uma destas opcoes:
|
|
|
|
- somente o administrador cria usuarios;
|
|
- cadastro exige um codigo de convite;
|
|
- cadastro publico exige verificacao de e-mail.
|
|
|
|
Nao deixe cadastro publico ilimitado em uma aplicacao que usa sua chave da API.
|
|
|
|
## 9. Historico de conversas
|
|
|
|
### Fluxo
|
|
|
|
1. O usuario cria uma conversa.
|
|
2. A aplicacao cria um registro `Conversation`.
|
|
3. A mensagem do usuario e salva em `Message`.
|
|
4. O backend carrega as mensagens anteriores daquela conversa.
|
|
5. O backend chama a OpenAI.
|
|
6. A resposta e salva como outra `Message`.
|
|
7. O registro de consumo e salvo.
|
|
8. A tela atualiza a lista e o horario da conversa.
|
|
|
|
### Regra de propriedade
|
|
|
|
Toda consulta deve combinar o identificador da conversa com o usuario autenticado:
|
|
|
|
```ts
|
|
const conversation = await prisma.conversation.findFirst({
|
|
where: {
|
|
id: conversationId,
|
|
userId: session.user.id,
|
|
},
|
|
});
|
|
```
|
|
|
|
Buscar somente pelo `conversationId` criaria uma falha de seguranca: um usuario poderia tentar abrir a conversa de outro usuario alterando a URL.
|
|
|
|
### Contexto enviado ao modelo
|
|
|
|
Nao envie para a API todas as mensagens de uma conversa sem limite. Conversas grandes:
|
|
|
|
- aumentam o custo;
|
|
- ficam mais lentas;
|
|
- podem ultrapassar o limite de contexto do modelo.
|
|
|
|
Uma estrategia inicial e enviar as mensagens mais recentes. Depois, implemente resumo automatico das mensagens antigas e armazene esse resumo na conversa.
|
|
|
|
## 10. Upload de arquivos
|
|
|
|
### Fluxo seguro
|
|
|
|
1. exigir usuario autenticado;
|
|
2. limitar o tamanho;
|
|
3. permitir somente extensoes e tipos necessarios;
|
|
4. conferir o tipo real do arquivo;
|
|
5. gerar nome aleatorio para armazenamento;
|
|
6. calcular SHA-256;
|
|
7. salvar fora da pasta publica;
|
|
8. registrar os metadados no banco;
|
|
9. enviar para a OpenAI apenas quando for usado em uma mensagem;
|
|
10. apagar arquivos temporarios e arquivos remotos quando nao forem mais necessarios.
|
|
|
|
Exemplo de nome fisico:
|
|
|
|
```text
|
|
/var/lib/meu-gpt/uploads/usuario-id/550e8400-e29b-41d4-a716-446655440000.pdf
|
|
```
|
|
|
|
O nome original fica no banco para ser exibido na interface, mas nao deve ser usado diretamente como caminho no disco.
|
|
|
|
### Tipos iniciais recomendados
|
|
|
|
Comece com poucos formatos:
|
|
|
|
- PDF;
|
|
- TXT;
|
|
- CSV;
|
|
- DOCX, se o fluxo escolhido oferecer suporte adequado;
|
|
- imagens PNG e JPEG, se o modelo selecionado aceitar imagens.
|
|
|
|
Defina um limite menor na propria aplicacao, por exemplo 10 MB, mesmo que a API aceite arquivos maiores. Isso protege o disco, a memoria e o custo.
|
|
|
|
### Dois usos diferentes de arquivos
|
|
|
|
**Arquivo anexado a uma pergunta:** o documento e enviado como entrada daquela mensagem. E a opcao mais simples para a primeira versao.
|
|
|
|
**Biblioteca permanente de documentos:** exige busca nos documentos, controle de permissoes, indexacao e normalmente uma estrategia de RAG ou busca em arquivos. Deve ser uma segunda fase.
|
|
|
|
## 11. Controle de custo por usuario
|
|
|
|
### Como medir
|
|
|
|
A resposta da OpenAI informa dados de uso, incluindo tokens de entrada, saida e total. O backend deve salvar esses valores em `UsageRecord`.
|
|
|
|
Exemplo conceitual:
|
|
|
|
```ts
|
|
const usage = response.usage;
|
|
|
|
await prisma.usageRecord.create({
|
|
data: {
|
|
userId,
|
|
conversationId,
|
|
model: response.model,
|
|
inputTokens: usage?.input_tokens ?? 0,
|
|
cachedInputTokens: usage?.input_tokens_details?.cached_tokens ?? 0,
|
|
outputTokens: usage?.output_tokens ?? 0,
|
|
reasoningTokens: usage?.output_tokens_details?.reasoning_tokens ?? 0,
|
|
totalTokens: usage?.total_tokens ?? 0,
|
|
costUsd: calculatedCost,
|
|
status: "SUCCESS",
|
|
requestId: response.id,
|
|
},
|
|
});
|
|
```
|
|
|
|
Os nomes exatos dos campos devem ser conferidos na versao do SDK instalada.
|
|
|
|
### Como calcular
|
|
|
|
Mantenha no backend uma tabela de precos por modelo:
|
|
|
|
```ts
|
|
type ModelPrice = {
|
|
inputUsdPerMillion: number;
|
|
cachedInputUsdPerMillion?: number;
|
|
outputUsdPerMillion: number;
|
|
};
|
|
```
|
|
|
|
Formula geral:
|
|
|
|
```text
|
|
custo de entrada =
|
|
tokens de entrada cobraveis / 1.000.000 x preco de entrada
|
|
|
|
custo de entrada em cache =
|
|
tokens em cache / 1.000.000 x preco de cache
|
|
|
|
custo de saida =
|
|
tokens de saida / 1.000.000 x preco de saida
|
|
|
|
custo total =
|
|
entrada + cache + saida
|
|
```
|
|
|
|
Nao grave valores monetarios usando `float`. Use `Decimal` no banco para evitar erros de arredondamento.
|
|
|
|
Os precos mudam. Por isso:
|
|
|
|
- nao espalhe precos pelo codigo;
|
|
- mantenha uma unica tabela de configuracao;
|
|
- registre o modelo usado em cada chamada;
|
|
- revise os precos na pagina oficial antes de atualizar;
|
|
- trate o valor mostrado como estimativa interna e reconcilie-o periodicamente com o painel da OpenAI.
|
|
|
|
### Verificacao do limite
|
|
|
|
Antes de chamar a API:
|
|
|
|
```ts
|
|
const startOfMonth = new Date(
|
|
new Date().getFullYear(),
|
|
new Date().getMonth(),
|
|
1,
|
|
);
|
|
|
|
const usage = await prisma.usageRecord.aggregate({
|
|
where: {
|
|
userId,
|
|
status: "SUCCESS",
|
|
createdAt: { gte: startOfMonth },
|
|
},
|
|
_sum: { costUsd: true },
|
|
});
|
|
```
|
|
|
|
Se o total for maior ou igual ao limite, a API deve responder:
|
|
|
|
```json
|
|
{
|
|
"error": "Limite mensal atingido"
|
|
}
|
|
```
|
|
|
|
Use o status HTTP `429` para indicar que o limite de uso foi atingido.
|
|
|
|
### Evitar que chamadas simultaneas ultrapassem o limite
|
|
|
|
Uma simples verificacao antes da chamada nao e suficiente quando o mesmo usuario envia varias requisicoes ao mesmo tempo.
|
|
|
|
Para um limite mais rigido:
|
|
|
|
- permita somente uma geracao ativa por usuario; ou
|
|
- reserve um valor estimado antes da chamada em uma transacao;
|
|
- ao terminar, substitua a reserva pelo custo real;
|
|
- libere a reserva se a chamada falhar.
|
|
|
|
Tambem defina `max_output_tokens` para limitar o tamanho e o custo maximo da resposta.
|
|
|
|
### O painel de consumo
|
|
|
|
Mostre ao usuario:
|
|
|
|
- consumo do mes;
|
|
- limite mensal;
|
|
- percentual utilizado;
|
|
- tokens de entrada e saida;
|
|
- consumo por conversa;
|
|
- modelo utilizado;
|
|
- data de renovacao do limite.
|
|
|
|
O administrador deve visualizar os mesmos dados para todos os usuarios.
|
|
|
|
## 12. Fluxo da rota de chat
|
|
|
|
Uma rota segura segue esta ordem:
|
|
|
|
```text
|
|
1. Validar a sessao
|
|
2. Validar os dados recebidos
|
|
3. Confirmar que a conversa pertence ao usuario
|
|
4. Confirmar que o usuario esta ativo
|
|
5. Conferir o limite mensal
|
|
6. Salvar a mensagem do usuario
|
|
7. Preparar contexto e anexos
|
|
8. Chamar a OpenAI
|
|
9. Salvar a resposta
|
|
10. Salvar tokens e custo
|
|
11. Devolver a resposta ao navegador
|
|
```
|
|
|
|
Pseudoimplementacao:
|
|
|
|
```ts
|
|
export async function POST(request: Request) {
|
|
const session = await requireSession();
|
|
const input = chatSchema.parse(await request.json());
|
|
|
|
const conversation = await findOwnedConversation(
|
|
input.conversationId,
|
|
session.user.id,
|
|
);
|
|
|
|
await assertUserCanSpend(session.user.id);
|
|
|
|
const userMessage = await saveUserMessage(conversation.id, input);
|
|
|
|
try {
|
|
const response = await createOpenAIResponse({
|
|
conversation,
|
|
userMessage,
|
|
});
|
|
|
|
const calculatedCost = calculateCost(response.model, response.usage);
|
|
|
|
await saveAssistantMessageAndUsage({
|
|
userId: session.user.id,
|
|
conversationId: conversation.id,
|
|
response,
|
|
calculatedCost,
|
|
});
|
|
|
|
return Response.json({ text: response.output_text });
|
|
} catch (error) {
|
|
await saveFailedUsage(session.user.id, conversation.id);
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
No codigo real, salve a mensagem do assistente e o consumo na mesma transacao do banco.
|
|
|
|
## 13. Painel administrativo
|
|
|
|
Crie uma area acessivel somente quando:
|
|
|
|
```ts
|
|
session.user.role === "ADMIN"
|
|
```
|
|
|
|
Funcoes iniciais:
|
|
|
|
- listar usuarios;
|
|
- bloquear e desbloquear;
|
|
- alterar limite mensal;
|
|
- promover um usuario a administrador;
|
|
- consultar consumo por periodo;
|
|
- consultar erros;
|
|
- excluir ou anonimizar um usuario;
|
|
- apagar anexos e conversas conforme a politica definida.
|
|
|
|
Toda alteracao administrativa importante deve gerar um registro de auditoria em uma futura tabela `AuditLog`.
|
|
|
|
## 14. Seguranca essencial
|
|
|
|
Antes de publicar:
|
|
|
|
- use HTTPS;
|
|
- mantenha a chave da OpenAI somente no servidor;
|
|
- use hash forte de senha;
|
|
- aplique limite de tentativas no login;
|
|
- aplique limite de requisicoes no chat e upload;
|
|
- valide todos os dados com `zod`;
|
|
- confira a propriedade de cada conversa e arquivo;
|
|
- limite tamanho e tipo dos uploads;
|
|
- use nomes aleatorios no disco;
|
|
- nao renderize HTML recebido do modelo sem sanitizacao;
|
|
- configure cookies como `HttpOnly`, `Secure` e `SameSite`;
|
|
- bloqueie usuarios desativados em todas as APIs;
|
|
- mantenha dependencias atualizadas;
|
|
- faca backup criptografado;
|
|
- nunca inclua segredos nos logs;
|
|
- defina politica de retencao e exclusao de dados.
|
|
|
|
Se a aplicacao ficar acessivel somente por VPN, a superficie de ataque diminui, mas login e autorizacao continuam necessarios.
|
|
|
|
## 15. Preparar armazenamento no LXC
|
|
|
|
Exemplo:
|
|
|
|
```bash
|
|
mkdir -p /var/lib/meu-gpt/uploads
|
|
chown -R meu-gpt:meu-gpt /var/lib/meu-gpt
|
|
chmod 750 /var/lib/meu-gpt
|
|
chmod 750 /var/lib/meu-gpt/uploads
|
|
```
|
|
|
|
O usuario Linux `meu-gpt` deve ser o mesmo que executa o processo da aplicacao.
|
|
|
|
Evite executar a aplicacao como `root`.
|
|
|
|
## 16. Migrations e primeira conta administrativa
|
|
|
|
Depois de configurar o Prisma:
|
|
|
|
```bash
|
|
npx prisma generate
|
|
npx prisma migrate dev --name auth_history_upload_usage
|
|
```
|
|
|
|
Em producao, aplique migrations ja criadas:
|
|
|
|
```bash
|
|
npx prisma migrate deploy
|
|
```
|
|
|
|
Crie um script de seed para a primeira conta administrativa. A senha deve vir de uma variavel de ambiente temporaria, ser transformada em hash e nunca aparecer no repositorio.
|
|
|
|
## 17. Build e execucao
|
|
|
|
```bash
|
|
npm run build
|
|
pm2 start npm --name meu-gpt -- start
|
|
pm2 save
|
|
pm2 startup
|
|
```
|
|
|
|
Use o comando exibido pelo `pm2 startup` para ativar o inicio automatico.
|
|
|
|
Consulte logs:
|
|
|
|
```bash
|
|
pm2 logs meu-gpt
|
|
```
|
|
|
|
Os logs devem conter IDs de requisicao e mensagens de erro, mas nao senhas, tokens de sessao, chave da API ou conteudo confidencial dos arquivos.
|
|
|
|
## 18. Nginx
|
|
|
|
Exemplo basico:
|
|
|
|
```nginx
|
|
server {
|
|
listen 80;
|
|
server_name gpt.seudominio.com;
|
|
|
|
client_max_body_size 10M;
|
|
|
|
location / {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_http_version 1.1;
|
|
|
|
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;
|
|
}
|
|
}
|
|
```
|
|
|
|
O limite do Nginx deve ser igual ou ligeiramente maior que o limite configurado na aplicacao.
|
|
|
|
Depois:
|
|
|
|
```bash
|
|
nginx -t
|
|
systemctl reload nginx
|
|
certbot --nginx -d gpt.seudominio.com
|
|
```
|
|
|
|
## 19. Backup
|
|
|
|
O backup precisa incluir:
|
|
|
|
- banco PostgreSQL;
|
|
- pasta de uploads;
|
|
- arquivos de configuracao;
|
|
- migrations do Prisma.
|
|
|
|
Exemplo de backup do banco:
|
|
|
|
```bash
|
|
sudo -u postgres pg_dump -Fc meu_gpt > /var/backups/meu-gpt-db.dump
|
|
```
|
|
|
|
Nao basta criar o backup. Teste periodicamente a restauracao em outro banco ou container.
|
|
|
|
Considere snapshots do LXC como protecao adicional, mas mantenha tambem backups independentes do Proxmox.
|
|
|
|
## 20. Ordem recomendada de implementacao
|
|
|
|
### Fase 1: banco e login
|
|
|
|
- instalar PostgreSQL;
|
|
- adicionar Prisma;
|
|
- criar `User`;
|
|
- criar cadastro controlado;
|
|
- implementar login e logout;
|
|
- proteger as rotas.
|
|
|
|
### Fase 2: historico
|
|
|
|
- criar `Conversation` e `Message`;
|
|
- criar lista lateral;
|
|
- abrir, renomear, arquivar e excluir conversa;
|
|
- validar propriedade em todas as rotas.
|
|
|
|
### Fase 3: consumo
|
|
|
|
- criar `UsageRecord`;
|
|
- registrar tokens retornados pela OpenAI;
|
|
- centralizar precos;
|
|
- exibir consumo mensal;
|
|
- bloquear chamadas acima do limite;
|
|
- tratar requisicoes simultaneas.
|
|
|
|
### Fase 4: arquivos
|
|
|
|
- criar `Attachment`;
|
|
- adicionar upload privado;
|
|
- validar arquivo;
|
|
- anexar arquivo a uma mensagem;
|
|
- configurar exclusao e retencao.
|
|
|
|
### Fase 5: administracao e operacao
|
|
|
|
- criar painel administrativo;
|
|
- adicionar auditoria;
|
|
- configurar backups;
|
|
- configurar rate limit;
|
|
- revisar logs e alertas;
|
|
- testar restauracao.
|
|
|
|
## 21. Testes necessarios
|
|
|
|
### Autenticacao
|
|
|
|
- cadastro com e-mail duplicado;
|
|
- senha incorreta;
|
|
- usuario bloqueado;
|
|
- rota privada sem sessao;
|
|
- usuario comum tentando abrir o painel administrativo.
|
|
|
|
### Autorizacao
|
|
|
|
- usuario A tentando abrir conversa do usuario B;
|
|
- usuario A tentando baixar anexo do usuario B;
|
|
- alteracao manual de IDs na URL e no corpo da requisicao.
|
|
|
|
### Consumo
|
|
|
|
- chamada registra tokens;
|
|
- custo usa o modelo correto;
|
|
- limite bloqueia nova chamada;
|
|
- mudanca de mes reinicia a soma;
|
|
- duas chamadas simultaneas nao ultrapassam o limite rigido;
|
|
- erro da OpenAI nao e cobrado incorretamente pelo sistema.
|
|
|
|
### Upload
|
|
|
|
- arquivo acima do limite;
|
|
- extensao permitida com conteudo de outro tipo;
|
|
- nome contendo `../`;
|
|
- arquivo vazio;
|
|
- arquivo de outro usuario;
|
|
- exclusao do arquivo local e do registro correspondente.
|
|
|
|
## 22. Melhorias futuras
|
|
|
|
- verificacao de e-mail;
|
|
- recuperacao de senha;
|
|
- autenticacao em dois fatores;
|
|
- login corporativo;
|
|
- streaming da resposta;
|
|
- busca nas conversas;
|
|
- exportacao de conversa;
|
|
- RAG para biblioteca de documentos;
|
|
- MinIO para armazenamento;
|
|
- Redis para rate limit e controle de concorrencia;
|
|
- antivirus para uploads;
|
|
- alertas de consumo;
|
|
- observabilidade e auditoria;
|
|
- separacao do PostgreSQL em outro LXC.
|
|
|
|
## 23. O que muda em relacao ao projeto original
|
|
|
|
| Parte | Projeto original | Projeto v2 |
|
|
|---|---|---|
|
|
| Acesso | Uma senha compartilhada | Conta individual e sessao |
|
|
| Usuarios | Nao possui | Salvos no PostgreSQL |
|
|
| Conversas | Somente na tela | Salvas no banco |
|
|
| Arquivos | Nao possui | Upload privado e metadados |
|
|
| Custos | Sem controle individual | Tokens e custo por usuario |
|
|
| Administracao | Nao possui | Usuarios, limites e consumo |
|
|
| Seguranca | Basica | Autenticacao, autorizacao e rate limit |
|
|
| Backup | Nao detalhado | Banco, arquivos e restauracao |
|
|
|
|
## 24. Referencias para estudo
|
|
|
|
- [OpenAI: inicio rapido da API](https://platform.openai.com/docs/quickstart)
|
|
- [OpenAI: referencia da Responses API](https://platform.openai.com/docs/api-reference/responses)
|
|
- [OpenAI: arquivos](https://platform.openai.com/docs/api-reference/files)
|
|
- [OpenAI: uso da API](https://platform.openai.com/docs/api-reference/usage)
|
|
- [OpenAI: precos da API](https://openai.com/api/pricing/)
|
|
- [OpenAI: controles e retencao de dados](https://platform.openai.com/docs/models/default-usage-policies-by-endpoint)
|
|
- [Next.js: Route Handlers](https://nextjs.org/docs/app/getting-started/route-handlers)
|
|
- [Prisma: integracao com Next.js](https://www.prisma.io/docs/guides/frameworks/nextjs)
|
|
- [Auth.js: documentacao](https://authjs.dev/)
|
|
- [PostgreSQL: documentacao](https://www.postgresql.org/docs/)
|
|
|
|
## 25. Resumo
|
|
|
|
A evolucao principal e transformar uma pagina de chat protegida por senha em um sistema multiusuario.
|
|
|
|
O Next.js continua cuidando das telas e do backend. O PostgreSQL passa a guardar usuarios, conversas, mensagens, anexos e consumo. O Prisma facilita o acesso ao banco. O Auth.js cuida do login e da sessao. Os arquivos ficam em uma pasta privada, e cada resposta da OpenAI gera um registro de tokens e custo.
|
|
|
|
A melhor ordem de aprendizado e:
|
|
|
|
```text
|
|
login -> historico -> consumo -> upload -> painel administrativo
|
|
```
|
|
|
|
Essa ordem permite testar cada parte antes de adicionar a proxima e reduz a dificuldade de encontrar erros.
|