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
- A VPS Ubuntu foi acessada via SSH com chave PEM.
- O Nginx foi instalado com apt e ativado no sistema.
- A porta 80 foi liberada no Security Group da AWS para permitir acesso publico.
- O Node.js, npm e SQLite foram instalados na VPS.
- O projeto foi criado em /opt/sync-chat.
- Foram criadas paginas separadas para inicio, login, cadastro, chat e desenvolvimento.
- O backend Node.js foi criado para servir arquivos e rotas de API.
- O SQLite foi configurado para criar tabelas automaticamente quando o app inicia.
- O Nginx foi configurado como proxy reverso para o Node.js.
- 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 })
});
});