Sync Chat

DESENVOLVIMENTO

Documentacao tecnica simples do projeto Sync Chat.

Resumo do projeto

O Sync Chat e um sistema de mensagens online criado para rodar em uma VPS Ubuntu na Amazon EC2. Ele possui cadastro com termos de responsabilidade, login, chat com mensagens, imagens, audios, usuarios online e salas privadas.

A ideia principal foi criar um aplicativo leve, direto e facil de manter, usando tecnologias simples e conhecidas.

Tecnologias usadas

  • HTML: estrutura das paginas, como login, cadastro, chat e esta documentacao.
  • CSS: visual do sistema, cores, layout, responsividade e estilo das mensagens.
  • JavaScript no navegador: envia formularios, carrega mensagens, troca salas e envia arquivos.
  • Node.js: backend que recebe requisicoes, valida usuarios e conversa com o banco.
  • SQLite: banco de dados em arquivo, usado para guardar usuarios, salas, membros e mensagens.
  • Nginx: servidor web publico que recebe acessos na porta 80 e encaminha para o Node.js.
  • systemd: gerenciador do Ubuntu que mantem o Sync Chat rodando como servico.

Arquitetura

Quando alguem acessa o IP publico, o pedido chega primeiro no Nginx. O Nginx encaminha para o aplicativo Node.js, que roda internamente na porta 3000. O Node.js serve as paginas e tambem responde as APIs do sistema.

Usuario no navegador
       |
       v
IP publico da VPS - porta 80
       |
       v
Nginx
       |
       v
Node.js - porta 3000
       |
       v
SQLite - arquivo do banco

Passo a passo de criacao

  1. A VPS Ubuntu foi acessada via SSH com chave PEM.
  2. O Nginx foi instalado com apt e ativado no sistema.
  3. A porta 80 foi liberada no Security Group da AWS para permitir acesso publico.
  4. O Node.js, npm e SQLite foram instalados na VPS.
  5. O projeto foi criado em /opt/sync-chat.
  6. Foram criadas paginas separadas para inicio, login, cadastro, chat e desenvolvimento.
  7. O backend Node.js foi criado para servir arquivos e rotas de API.
  8. O SQLite foi configurado para criar tabelas automaticamente quando o app inicia.
  9. O Nginx foi configurado como proxy reverso para o Node.js.
  10. O app foi registrado como servico sync-chat no systemd.

Cadastro e login

No cadastro, o usuario informa nome, usuario e senha. Ele tambem precisa aceitar os termos de responsabilidade. Se nao aceitar, o sistema bloqueia o cadastro.

A senha nao e salva pura no banco. O servidor cria um hash usando criptografia do Node.js. Depois do cadastro, o sistema faz login automaticamente e envia o usuario para o chat.

Cadastro -> valida dados -> aceita termos -> salva usuario -> login automatico -> chat

Como o chat funciona

O navegador chama a API de mensagens a cada poucos segundos para buscar novidades. Quando uma mensagem e enviada, o JavaScript manda os dados para o servidor, o servidor grava no SQLite e o chat atualiza a conversa.

  • Mensagens de texto sao salvas com tipo text.
  • Imagens sao enviadas pelo navegador em formato base64 e salvas como tipo image.
  • Audios sao enviados da mesma forma e salvos como tipo audio.
  • Cada mensagem pertence a uma sala especifica.

Salas privadas

O sistema possui uma sala publica padrao chamada Talk Live Room. Alem dela, o usuario pode criar salas privadas e convidar outros usuarios pelo nome de usuario.

Para proteger as salas, o backend verifica se o usuario participa daquela sala antes de permitir listar ou enviar mensagens.

Usuarios online

Enquanto o usuario esta no chat, o navegador envia periodicamente um sinal chamado heartbeat para o servidor. Esse sinal atualiza o horario da ultima atividade do usuario.

Quem enviou sinal recentemente aparece na lista Online agora. Se o usuario fechar o navegador ou ficar sem atividade, ele some automaticamente depois de um tempo.

Banco de dados

O banco fica salvo na VPS neste caminho:

/opt/sync-chat/data/sync-chat.sqlite

Principais tabelas:

  • users: usuarios cadastrados, senha criptografada e aceite dos termos.
  • rooms: salas publicas e privadas.
  • room_members: quais usuarios participam de cada sala.
  • messages: mensagens de texto, imagem e audio.

Como fica online

O Sync Chat se mantem online porque roda como servico do Ubuntu. O servico se chama sync-chat. Ele foi configurado para iniciar junto com a VPS e reiniciar automaticamente se cair.

sudo systemctl status sync-chat
sudo systemctl restart sync-chat
sudo journalctl -u sync-chat -n 80 --no-pager

O Nginx fica sempre ouvindo a porta 80 e entregando o site para quem acessa o IP publico.

Estrutura dos arquivos

/opt/sync-chat
  server.js
  public/
    index.html
    login.html
    cadastro.html
    chat.html
    desenvolvimento.html
    styles.css
    common.js
    login.js
    cadastro.js
    chat.js
    logo-original.png
    logo.svg
  data/
    sync-chat.sqlite

Codigo fonte essencial

Estes sao os trechos principais que fazem o site funcionar: banco, seguranca de senha, login, mensagens, painel administrativo e frontend do chat.

Banco de dados SQLite

CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  username TEXT NOT NULL UNIQUE,
  password_hash TEXT NOT NULL,
  role TEXT NOT NULL DEFAULT 'Membro',
  terms_accepted INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS messages (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  room_id INTEGER NOT NULL DEFAULT 1,
  user_id INTEGER NOT NULL,
  body TEXT NOT NULL,
  type TEXT NOT NULL DEFAULT 'text',
  media_data TEXT,
  media_name TEXT
);

CREATE TABLE IF NOT EXISTS tags (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL UNIQUE,
  color TEXT NOT NULL DEFAULT '#176b87'
);

Criptografia de senha

function hashPassword(password, salt = crypto.randomBytes(16).toString("hex")) {
  const hash = crypto.scryptSync(password, salt, 64).toString("hex");
  return `${salt}:${hash}`;
}

function verifyPassword(password, stored) {
  const [salt, hash] = stored.split(":");
  const check = hashPassword(password, salt).split(":")[1];
  return crypto.timingSafeEqual(Buffer.from(hash, "hex"), Buffer.from(check, "hex"));
}

Login e sessao

const rows = await queryJson(
  "SELECT id, name, username, password_hash, role FROM users WHERE username = ?1;",
  [username]
);

if (!rows[0] || !verifyPassword(password, rows[0].password_hash)) {
  return json(res, 401, { error: "Usuario ou senha invalidos." });
}

const token = crypto.randomBytes(32).toString("hex");
sessions.set(token, { user: logged, createdAt: Date.now() });

Mensagens por sala

const roomId = validPositiveId(body.roomId, 1);
if (!(await canAccessRoom(user.id, roomId))) {
  return json(res, 403, { error: "Voce nao participa dessa sala." });
}

await runParameterized(
  "INSERT INTO messages (room_id, user_id, body, type, media_data, media_name) VALUES (?1, ?2, ?3, ?4, ?5, ?6);",
  [roomId, user.id, text, type, mediaData, mediaName]
);

Painel administrativo

function requireAdmin(user) {
  if (!user || user.role !== "ADM") {
    const error = new Error("Acesso restrito a ADM.");
    error.statusCode = 403;
    throw error;
  }
}

await runParameterized("UPDATE users SET role = ?1 WHERE id = ?2;", [role, targetId]);
await runParameterized("UPDATE users SET password_hash = ?1 WHERE id = ?2;", [hashPassword(newPassword), targetId]);

Frontend do chat

async function loadMessages() {
  const data = await api(`/api/messages?room=${activeRoom.id}`);
  renderMessages(data.messages);
}

messageForm.addEventListener("submit", async (event) => {
  event.preventDefault();
  await api("/api/messages", {
    method: "POST",
    body: JSON.stringify({ roomId: activeRoom.id, body, type, mediaData, mediaName })
  });
});