Initial commit
This commit is contained in:
412
api/src/server.js
Normal file
412
api/src/server.js
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user