Initial commit

This commit is contained in:
2025-11-28 07:49:17 -03:00
commit d84803efc9
12 changed files with 2074 additions and 0 deletions

14
api/Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM node:20-alpine
WORKDIR /app
# Instala dependências
COPY package.json package-lock.json* ./
RUN npm install --production || npm install --production --no-audit --no-fund
# Copia código
COPY src ./src
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]

17
api/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "telseg-dashboard-api",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2",
"pg": "^8.11.3",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2"
}
}

412
api/src/server.js Normal file
View File

@@ -0,0 +1,412 @@
import express from 'express';
import cors from 'cors';
import pkg from 'pg';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
const { Pool } = pkg;
const PORT = process.env.PORT || 3000;
const DATABASE_URL = process.env.DATABASE_URL || `postgres://${process.env.POSTGRES_USER || 'telseg'}:${process.env.POSTGRES_PASSWORD || 'telseg'}@${process.env.DB_HOST || 'db'}:${process.env.DB_PORT || 5432}/${process.env.POSTGRES_DB || 'telseg'}`;
const JWT_SECRET = process.env.JWT_SECRET || 'devsecret';
const ADMIN_USER = process.env.ADMIN_USER || 'admin';
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
const pool = new Pool({ connectionString: DATABASE_URL });
async function initDb() {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(`CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('master','user')),
created_at TIMESTAMPTZ DEFAULT now()
);`);
await client.query(`CREATE TABLE IF NOT EXISTS permissions (
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
units_read BOOLEAN DEFAULT false,
units_write BOOLEAN DEFAULT false,
creds_read BOOLEAN DEFAULT false,
creds_write BOOLEAN DEFAULT false,
notes_read BOOLEAN DEFAULT false,
notes_write BOOLEAN DEFAULT false
);`);
await client.query(`CREATE TABLE IF NOT EXISTS units (
id TEXT PRIMARY KEY,
nome TEXT NOT NULL,
data_criacao DATE,
ocs TEXT[],
link TEXT,
ip TEXT,
login TEXT,
senha TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);`);
await client.query(`CREATE TABLE IF NOT EXISTS creds (
id TEXT PRIMARY KEY,
nome TEXT NOT NULL,
usuario TEXT,
url TEXT,
notas TEXT,
password_enc JSONB NOT NULL,
updated_at BIGINT,
created_at TIMESTAMPTZ DEFAULT now()
);`);
await client.query(`CREATE TABLE IF NOT EXISTS vault (
id INTEGER PRIMARY KEY,
salt TEXT NOT NULL,
verification JSONB NOT NULL,
iterations INTEGER NOT NULL,
v INTEGER DEFAULT 1
);`);
await client.query(`CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
title TEXT,
body TEXT,
data JSONB,
updated_at BIGINT,
created_at TIMESTAMPTZ DEFAULT now()
);`);
await client.query(`ALTER TABLE notes ADD COLUMN IF NOT EXISTS data JSONB;`);
await client.query(`ALTER TABLE units ADD COLUMN IF NOT EXISTS login TEXT;`);
await client.query(`ALTER TABLE units ADD COLUMN IF NOT EXISTS senha TEXT;`);
await client.query('COMMIT');
// Seed admin user if none exists
const { rows: cnt } = await pool.query('SELECT COUNT(*)::int AS c FROM users');
if (cnt[0].c === 0) {
const id = genId();
const hash = await bcrypt.hash(ADMIN_PASSWORD, 10);
await pool.query('INSERT INTO users (id, username, password_hash, role) VALUES ($1,$2,$3,$4)', [id, ADMIN_USER, hash, 'master']);
await pool.query('INSERT INTO permissions (user_id, units_read, units_write, creds_read, creds_write, notes_read, notes_write) VALUES ($1,true,true,true,true,true,true)', [id]);
console.log('Admin user created:', ADMIN_USER);
}
} catch (e) {
await client.query('ROLLBACK');
console.error('DB init error:', e);
throw e;
} finally {
client.release();
}
}
function genId() {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`;
}
function normalizePerms(p = {}) {
return {
units_read: !!p.units_read,
units_write: !!p.units_write,
creds_read: !!p.creds_read,
creds_write: !!p.creds_write,
notes_read: !!p.notes_read,
notes_write: !!p.notes_write,
};
}
async function loadUserById(id) {
const { rows } = await pool.query('SELECT id, username, role FROM users WHERE id=$1', [id]);
if (!rows.length) return null;
const u = rows[0];
const { rows: pr } = await pool.query('SELECT * FROM permissions WHERE user_id=$1', [id]);
return { id: u.id, username: u.username, role: u.role, permissions: normalizePerms(pr[0]) };
}
function signToken(user) {
return jwt.sign({ sub: user.id, role: user.role }, JWT_SECRET, { expiresIn: '7d' });
}
function mapUnit(row) {
return {
id: row.id,
nome: row.nome,
dataCriacao: row.data_criacao ? row.data_criacao.toISOString().slice(0,10) : null,
ocs: row.ocs || [],
link: row.link || '',
ip: row.ip || '',
login: row.login || '',
senha: row.senha || ''
};
}
function mapCred(row) {
return {
id: row.id,
nome: row.nome,
usuario: row.usuario || '',
url: row.url || '',
notas: row.notas || '',
passwordEnc: row.password_enc,
updatedAt: Number(row.updated_at || 0)
};
}
const app = express();
app.use(cors());
app.use(express.json({ limit: '1mb' }));
app.get('/api/health', (_req, res) => {
res.json({ ok: true });
});
// Auth helpers
async function authRequired(req, res, next) {
const auth = req.headers.authorization || '';
const token = auth.startsWith('Bearer ') ? auth.slice(7) : null;
if (!token) return res.status(401).json({ error: 'unauthorized' });
try {
const payload = jwt.verify(token, JWT_SECRET);
const user = await loadUserById(payload.sub);
if (!user) return res.status(401).json({ error: 'unauthorized' });
req.user = user; next();
} catch {
return res.status(401).json({ error: 'unauthorized' });
}
}
function requireMaster(req, res, next) {
if (req.user?.role === 'master') return next();
return res.status(403).json({ error: 'forbidden' });
}
function requirePermFlag(flag) {
return (req, res, next) => {
if (req.user?.role === 'master') return next();
if (req.user?.permissions?.[flag]) return next();
return res.status(403).json({ error: 'forbidden' });
};
}
// Auth endpoints
app.post('/api/auth/login', async (req, res) => {
const { username, password } = req.body || {};
if (!username || !password) return res.status(400).json({ error: 'missing' });
const { rows } = await pool.query('SELECT * FROM users WHERE username=$1', [username]);
if (!rows.length) return res.status(401).json({ error: 'invalid' });
const u = rows[0];
const ok = await bcrypt.compare(password, u.password_hash);
if (!ok) return res.status(401).json({ error: 'invalid' });
const token = signToken(u);
const user = await loadUserById(u.id);
res.json({ token, user });
});
app.get('/api/auth/me', authRequired, (req, res) => {
res.json({ user: req.user });
});
// Users management (master only)
app.get('/api/users', authRequired, requireMaster, async (_req, res) => {
const { rows } = await pool.query('SELECT id, username, role, created_at FROM users ORDER BY created_at DESC');
const list = [];
for (const r of rows) {
const { rows: pr } = await pool.query('SELECT * FROM permissions WHERE user_id=$1', [r.id]);
list.push({ id: r.id, username: r.username, role: r.role, permissions: normalizePerms(pr[0] || {}) });
}
res.json(list);
});
app.post('/api/users', authRequired, requireMaster, async (req, res) => {
const { username, password, role, permissions } = req.body || {};
if (!username || !password) return res.status(400).json({ error: 'missing' });
const id = genId();
const hash = await bcrypt.hash(password, 10);
await pool.query('INSERT INTO users (id, username, password_hash, role) VALUES ($1,$2,$3,$4)', [id, username, hash, role === 'master' ? 'master' : 'user']);
const p = normalizePerms(permissions || {});
await pool.query('INSERT INTO permissions (user_id, units_read, units_write, creds_read, creds_write, notes_read, notes_write) VALUES ($1,$2,$3,$4,$5,$6,$7)', [id, p.units_read, p.units_write, p.creds_read, p.creds_write, p.notes_read, p.notes_write]);
res.status(201).json({ ok: true, id });
});
app.put('/api/users/:id', authRequired, requireMaster, async (req, res) => {
const id = req.params.id; const { password, role } = req.body || {};
if (password) {
const hash = await bcrypt.hash(password, 10);
await pool.query('UPDATE users SET password_hash=$2 WHERE id=$1', [id, hash]);
}
if (role) await pool.query('UPDATE users SET role=$2 WHERE id=$1', [id, role === 'master' ? 'master' : 'user']);
res.json({ ok: true });
});
app.put('/api/users/:id/permissions', authRequired, requireMaster, async (req, res) => {
const id = req.params.id; const p = normalizePerms(req.body || {});
await pool.query('INSERT INTO permissions (user_id, units_read, units_write, creds_read, creds_write, notes_read, notes_write) VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT (user_id) DO UPDATE SET units_read=EXCLUDED.units_read, units_write=EXCLUDED.units_write, creds_read=EXCLUDED.creds_read, creds_write=EXCLUDED.creds_write, notes_read=EXCLUDED.notes_read, notes_write=EXCLUDED.notes_write', [id, p.units_read, p.units_write, p.creds_read, p.creds_write, p.notes_read, p.notes_write]);
res.json({ ok: true });
});
app.delete('/api/users/:id', authRequired, requireMaster, async (req, res) => {
await pool.query('DELETE FROM users WHERE id=$1', [req.params.id]);
res.json({ ok: true });
});
// Units
app.get('/api/units', authRequired, requirePermFlag('units_read'), async (_req, res) => {
// Números primeiro, depois letras; em seguida ordem alfabética
const { rows } = await pool.query(
`SELECT * FROM units
ORDER BY (nome ~ '^[0-9]') DESC, nome ASC`
);
res.json(rows.map(mapUnit));
});
app.post('/api/units', authRequired, requirePermFlag('units_write'), async (req, res) => {
const u = req.body;
if (!u || !u.id || !u.nome) return res.status(400).json({ error: 'id e nome são obrigatórios' });
await pool.query(
`INSERT INTO units (id, nome, data_criacao, ocs, link, ip, login, senha) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
ON CONFLICT (id) DO NOTHING`,
[u.id, u.nome, u.dataCriacao || null, u.ocs || [], u.link || '', u.ip || '', u.login || '', u.senha || '']
);
res.status(201).json({ ok: true });
});
app.put('/api/units/:id', authRequired, requirePermFlag('units_write'), async (req, res) => {
const id = req.params.id;
const u = req.body || {};
await pool.query(
`UPDATE units SET nome=$2, data_criacao=$3, ocs=$4, link=$5, ip=$6, login=$7, senha=$8, updated_at=now() WHERE id=$1`,
[id, u.nome || '', u.dataCriacao || null, u.ocs || [], u.link || '', u.ip || '', u.login || '', u.senha || '']
);
res.json({ ok: true });
});
app.delete('/api/units/:id', authRequired, requirePermFlag('units_write'), async (req, res) => {
await pool.query('DELETE FROM units WHERE id=$1', [req.params.id]);
res.json({ ok: true });
});
// Creds
app.get('/api/creds', authRequired, requirePermFlag('creds_read'), async (_req, res) => {
const { rows } = await pool.query('SELECT * FROM creds ORDER BY updated_at DESC NULLS LAST, created_at DESC');
res.json(rows.map(mapCred));
});
app.post('/api/creds', authRequired, requirePermFlag('creds_write'), async (req, res) => {
const c = req.body;
if (!c || !c.id || !c.nome || !c.passwordEnc) return res.status(400).json({ error: 'id, nome e passwordEnc são obrigatórios' });
await pool.query(
`INSERT INTO creds (id, nome, usuario, url, notas, password_enc, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7)
ON CONFLICT (id) DO NOTHING`,
[c.id, c.nome, c.usuario || '', c.url || '', c.notas || '', c.passwordEnc, c.updatedAt || Date.now()]
);
res.status(201).json({ ok: true });
});
app.put('/api/creds/:id', authRequired, requirePermFlag('creds_write'), async (req, res) => {
const id = req.params.id;
const c = req.body || {};
await pool.query(
`UPDATE creds SET nome=$2, usuario=$3, url=$4, notas=$5, password_enc=$6, updated_at=$7 WHERE id=$1`,
[id, c.nome || '', c.usuario || '', c.url || '', c.notas || '', c.passwordEnc, c.updatedAt || Date.now()]
);
res.json({ ok: true });
});
app.delete('/api/creds/:id', authRequired, requirePermFlag('creds_write'), async (req, res) => {
await pool.query('DELETE FROM creds WHERE id=$1', [req.params.id]);
res.json({ ok: true });
});
// Notes
app.get('/api/notes', authRequired, requirePermFlag('notes_read'), async (_req, res) => {
const { rows } = await pool.query('SELECT * FROM notes ORDER BY updated_at DESC NULLS LAST, created_at DESC');
res.json(rows.map(r => ({ id: r.id, title: r.title || '', body: r.body || '', data: r.data || null, updatedAt: Number(r.updated_at || 0) })));
});
app.post('/api/notes', authRequired, requirePermFlag('notes_write'), async (req, res) => {
const n = req.body || {};
if (!n.id) return res.status(400).json({ error: 'id é obrigatório' });
await pool.query('INSERT INTO notes (id, title, body, data, updated_at) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (id) DO NOTHING', [n.id, n.title || '', n.body || '', n.data || null, n.updatedAt || Date.now()]);
res.status(201).json({ ok: true });
});
app.put('/api/notes/:id', authRequired, requirePermFlag('notes_write'), async (req, res) => {
const id = req.params.id; const n = req.body || {};
await pool.query('UPDATE notes SET title=$2, body=$3, data=$4, updated_at=$5 WHERE id=$1', [id, n.title || '', n.body || '', n.data || null, n.updatedAt || Date.now()]);
res.json({ ok: true });
});
app.delete('/api/notes/:id', authRequired, requirePermFlag('notes_write'), async (req, res) => {
await pool.query('DELETE FROM notes WHERE id=$1', [req.params.id]);
res.json({ ok: true });
});
// Vault meta
app.get('/api/vault', authRequired, requirePermFlag('creds_read'), async (_req, res) => {
const { rows } = await pool.query('SELECT * FROM vault WHERE id=1');
if (!rows.length) return res.json(null);
const v = rows[0];
res.json({ salt: v.salt, verification: v.verification, iterations: v.iterations, v: v.v || 1 });
});
app.put('/api/vault', authRequired, requireMaster, async (req, res) => {
const v = req.body || {};
if (!v.salt || !v.verification) return res.status(400).json({ error: 'salt e verification são obrigatórios' });
await pool.query(
`INSERT INTO vault (id, salt, verification, iterations, v)
VALUES (1, $1, $2, $3, $4)
ON CONFLICT (id) DO UPDATE SET salt=EXCLUDED.salt, verification=EXCLUDED.verification, iterations=EXCLUDED.iterations, v=EXCLUDED.v`,
[v.salt, v.verification, v.iterations || 200000, v.v || 1]
);
res.json({ ok: true });
});
// Export/Import utilitários
app.get('/api/export', authRequired, requireMaster, async (_req, res) => {
const [units, creds, vault, notes] = await Promise.all([
pool.query('SELECT * FROM units'),
pool.query('SELECT * FROM creds'),
pool.query('SELECT * FROM vault WHERE id=1'),
pool.query('SELECT * FROM notes')
]);
res.json({
version: 1,
exportedAt: new Date().toISOString(),
units: units.rows.map(mapUnit),
creds: creds.rows.map(mapCred),
vault: vault.rows[0] ? { salt: vault.rows[0].salt, verification: vault.rows[0].verification, iterations: vault.rows[0].iterations, v: vault.rows[0].v || 1 } : null,
notes: notes.rows.map(r => ({ id: r.id, title: r.title || '', body: r.body || '', data: r.data || null, updatedAt: Number(r.updated_at || 0) }))
});
});
app.post('/api/import', authRequired, requireMaster, async (req, res) => {
const data = req.body || {};
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('DELETE FROM units');
await client.query('DELETE FROM creds');
await client.query('DELETE FROM notes');
if (data.vault) {
await client.query('DELETE FROM vault WHERE id=1');
await client.query('INSERT INTO vault (id, salt, verification, iterations, v) VALUES (1,$1,$2,$3,$4)', [data.vault.salt, data.vault.verification, data.vault.iterations || 200000, data.vault.v || 1]);
}
if (Array.isArray(data.units)) {
for (const u of data.units) {
await client.query('INSERT INTO units (id, nome, data_criacao, ocs, link, ip, login, senha) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)', [u.id, u.nome, u.dataCriacao || null, u.ocs || [], u.link || '', u.ip || '', u.login || '', u.senha || '']);
}
}
if (Array.isArray(data.creds)) {
for (const c of data.creds) {
await client.query('INSERT INTO creds (id, nome, usuario, url, notas, password_enc, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7)', [c.id, c.nome, c.usuario || '', c.url || '', c.notas || '', c.passwordEnc, c.updatedAt || Date.now()]);
}
}
if (Array.isArray(data.notes)) {
for (const n of data.notes) {
await client.query('INSERT INTO notes (id, title, body, data, updated_at) VALUES ($1,$2,$3,$4,$5)', [n.id, n.title || '', n.body || '', n.data || null, n.updatedAt || Date.now()]);
}
}
await client.query('COMMIT');
res.json({ ok: true });
} catch (e) {
await client.query('ROLLBACK');
console.error('Import error', e);
res.status(500).json({ error: 'import failed' });
} finally {
client.release();
}
});
initDb().then(() => {
app.listen(PORT, () => console.log(`API listening on :${PORT}`));
}).catch((e) => {
console.error('Failed to init DB', e);
process.exit(1);
});