feat(wireguard): harden security constraints and fix db manager UI

This commit is contained in:
xtcnet 2026-03-10 11:25:40 +07:00
parent b99b623355
commit e057aee8ba
3 changed files with 34 additions and 20 deletions

View file

@ -17,6 +17,7 @@ const internalAuditLog = {
const query = auditLogModel const query = auditLogModel
.query() .query()
.andWhere("user_id", access.token.getUserId(1))
.orderBy("created_on", "DESC") .orderBy("created_on", "DESC")
.orderBy("id", "DESC") .orderBy("id", "DESC")
.limit(100) .limit(100)
@ -49,6 +50,7 @@ const internalAuditLog = {
const query = auditLogModel const query = auditLogModel
.query() .query()
.andWhere("id", data.id) .andWhere("id", data.id)
.andWhere("user_id", access.token.getUserId(1))
.allowGraph("[user]") .allowGraph("[user]")
.first(); .first();

View file

@ -213,8 +213,7 @@ const internalWireguard = {
.select("wg_client.*", "wg_interface.name as interface_name") .select("wg_client.*", "wg_interface.name as interface_name")
.orderBy("wg_client.created_on", "desc"); .orderBy("wg_client.created_on", "desc");
// Filter by owner if not admin if (access) {
if (access && (!accessData || accessData.permission_visibility !== "all")) {
query.andWhere("wg_client.owner_user_id", access.token.getUserId(1)); query.andWhere("wg_client.owner_user_id", access.token.getUserId(1));
} }
@ -280,6 +279,10 @@ const internalWireguard = {
const allocatedIPs = existingClients.map((c) => c.ipv4_address); const allocatedIPs = existingClients.map((c) => c.ipv4_address);
const ipv4Address = wgHelpers.findNextAvailableIP(iface.ipv4_cidr, allocatedIPs); const ipv4Address = wgHelpers.findNextAvailableIP(iface.ipv4_cidr, allocatedIPs);
if (!ipv4Address) {
throw new Error("No available IP addresses remaining in this WireGuard server subnet.");
}
const clientData = { const clientData = {
name: data.name || "Unnamed Client", name: data.name || "Unnamed Client",
enabled: true, enabled: true,
@ -309,7 +312,7 @@ const internalWireguard = {
*/ */
async deleteClient(knex, clientId, access, accessData) { async deleteClient(knex, clientId, access, accessData) {
const query = knex("wg_client").where("id", clientId); const query = knex("wg_client").where("id", clientId);
if (access && (!accessData || accessData.permission_visibility !== "all")) { if (access) {
query.andWhere("owner_user_id", access.token.getUserId(1)); query.andWhere("owner_user_id", access.token.getUserId(1));
} }
const client = await query.first(); const client = await query.first();
@ -328,7 +331,7 @@ const internalWireguard = {
*/ */
async toggleClient(knex, clientId, enabled, access, accessData) { async toggleClient(knex, clientId, enabled, access, accessData) {
const query = knex("wg_client").where("id", clientId); const query = knex("wg_client").where("id", clientId);
if (access && (!accessData || accessData.permission_visibility !== "all")) { if (access) {
query.andWhere("owner_user_id", access.token.getUserId(1)); query.andWhere("owner_user_id", access.token.getUserId(1));
} }
const client = await query.first(); const client = await query.first();
@ -351,7 +354,7 @@ const internalWireguard = {
*/ */
async updateClient(knex, clientId, data, access, accessData) { async updateClient(knex, clientId, data, access, accessData) {
const query = knex("wg_client").where("id", clientId); const query = knex("wg_client").where("id", clientId);
if (access && (!accessData || accessData.permission_visibility !== "all")) { if (access) {
query.andWhere("owner_user_id", access.token.getUserId(1)); query.andWhere("owner_user_id", access.token.getUserId(1));
} }
const client = await query.first(); const client = await query.first();
@ -414,7 +417,17 @@ const internalWireguard = {
*/ */
async createInterface(knex, data, access, accessData) { async createInterface(knex, data, access, accessData) {
const existingIfaces = await knex("wg_interface").select("name", "listen_port"); const existingIfaces = await knex("wg_interface").select("name", "listen_port");
const newIndex = existingIfaces.length;
if (existingIfaces.length >= 100) {
throw new Error("Maximum limit of 100 WireGuard servers reached.");
}
// Find the lowest available index between 0 and 99
const usedPorts = new Set(existingIfaces.map(i => i.listen_port));
let newIndex = 0;
while (usedPorts.has(51820 + newIndex)) {
newIndex++;
}
const name = `wg${newIndex}`; const name = `wg${newIndex}`;
const listen_port = 51820 + newIndex; const listen_port = 51820 + newIndex;
@ -470,7 +483,7 @@ const internalWireguard = {
*/ */
async updateInterface(knex, id, data, access, accessData) { async updateInterface(knex, id, data, access, accessData) {
const query = knex("wg_interface").where("id", id); const query = knex("wg_interface").where("id", id);
if (access && (!accessData || accessData.permission_visibility !== "all")) { if (access) {
query.andWhere("owner_user_id", access.token.getUserId(1)); query.andWhere("owner_user_id", access.token.getUserId(1));
} }
const iface = await query.first(); const iface = await query.first();
@ -493,7 +506,7 @@ const internalWireguard = {
*/ */
async deleteInterface(knex, id, access, accessData) { async deleteInterface(knex, id, access, accessData) {
const query = knex("wg_interface").where("id", id); const query = knex("wg_interface").where("id", id);
if (access && (!accessData || accessData.permission_visibility !== "all")) { if (access) {
query.andWhere("owner_user_id", access.token.getUserId(1)); query.andWhere("owner_user_id", access.token.getUserId(1));
} }
const iface = await query.first(); const iface = await query.first();
@ -519,7 +532,7 @@ const internalWireguard = {
async updateInterfaceLinks(knex, id, linkedServers, access, accessData) { async updateInterfaceLinks(knex, id, linkedServers, access, accessData) {
// Verify ownership // Verify ownership
const query = knex("wg_interface").where("id", id); const query = knex("wg_interface").where("id", id);
if (access && (!accessData || accessData.permission_visibility !== "all")) { if (access) {
query.andWhere("owner_user_id", access.token.getUserId(1)); query.andWhere("owner_user_id", access.token.getUserId(1));
} }
const iface = await query.first(); const iface = await query.first();
@ -546,8 +559,7 @@ const internalWireguard = {
*/ */
async getInterfacesInfo(knex, access, accessData) { async getInterfacesInfo(knex, access, accessData) {
const query = knex("wg_interface").select("*"); const query = knex("wg_interface").select("*");
// Filter by owner if not admin if (access) {
if (access && (!accessData || accessData.permission_visibility !== "all")) {
query.andWhere("owner_user_id", access.token.getUserId(1)); query.andWhere("owner_user_id", access.token.getUserId(1));
} }
const ifaces = await query; const ifaces = await query;

View file

@ -44,10 +44,10 @@ export default function DatabaseManager() {
} }
}; };
const renderTableData = (data: any[], schema?: any[]) => { const renderTableData = (data: any[]) => {
if (!data || data.length === 0) return <div className="text-muted p-3">No data</div>; if (!data || data.length === 0) return <div className="text-muted p-3">No data</div>;
// In SQLite, raw SQL mapping might mismatch explicit schemas, so strictly read keys from the first row.
const columns = schema ? schema.map((s: any) => s.name || s.Field) : Object.keys(data[0]); const columns = Object.keys(data[0]);
return ( return (
<div className="table-responsive"> <div className="table-responsive">
@ -119,7 +119,7 @@ export default function DatabaseManager() {
<Row> <Row>
<Col md={3}> <Col md={3}>
<Card className="shadow-sm"> <Card className="shadow-sm">
<Card.Header className="bg-light fw-bold">Tables</Card.Header> <Card.Header className="bg-body-tertiary text-body fw-bold">Tables</Card.Header>
<div className="list-group list-group-flush" style={{ maxHeight: "70vh", overflowY: "auto" }}> <div className="list-group list-group-flush" style={{ maxHeight: "70vh", overflowY: "auto" }}>
{tablesLoading && <div className="p-3"><Loading /></div>} {tablesLoading && <div className="p-3"><Loading /></div>}
{tables?.map((table: string) => ( {tables?.map((table: string) => (
@ -136,7 +136,7 @@ export default function DatabaseManager() {
</Col> </Col>
<Col md={9}> <Col md={9}>
<Card className="shadow-sm"> <Card className="shadow-sm">
<Card.Header className="bg-light d-flex justify-content-between align-items-center"> <Card.Header className="bg-body-tertiary text-body d-flex justify-content-between align-items-center">
<h5 className="mb-0 fw-bold">{activeTable || "Select a table"}</h5> <h5 className="mb-0 fw-bold">{activeTable || "Select a table"}</h5>
{tableData && <Badge bg="secondary">{tableData.total} rows</Badge>} {tableData && <Badge bg="secondary">{tableData.total} rows</Badge>}
</Card.Header> </Card.Header>
@ -144,7 +144,7 @@ export default function DatabaseManager() {
{tableDataLoading ? ( {tableDataLoading ? (
<div className="p-5 d-flex justify-content-center"><Loading /></div> <div className="p-5 d-flex justify-content-center"><Loading /></div>
) : ( ) : (
tableData && renderTableData(tableData.rows, tableData.schema) tableData && renderTableData(tableData.rows)
)} )}
</Card.Body> </Card.Body>
</Card> </Card>
@ -154,7 +154,7 @@ export default function DatabaseManager() {
{activeTab === "query" && ( {activeTab === "query" && (
<Card className="shadow-sm"> <Card className="shadow-sm">
<Card.Header className="bg-light fw-bold">Execute SQL Query</Card.Header> <Card.Header className="bg-body-tertiary text-body fw-bold">Execute SQL Query</Card.Header>
<Card.Body> <Card.Body>
<div className="mb-3 border rounded"> <div className="mb-3 border rounded">
<CodeEditor <CodeEditor
@ -185,8 +185,8 @@ export default function DatabaseManager() {
{queryResult && ( {queryResult && (
<div className="mt-4"> <div className="mt-4">
<h5 className="mb-3 border-bottom pb-2">Results</h5> <h5 className="mb-3 border-bottom pb-2">Results</h5>
{Array.isArray(queryResult) ? renderTableData(queryResult) : ( {Array.isArray(queryResult) && queryResult.length > 0 ? renderTableData(queryResult) : (
<pre className="p-3 bg-light rounded border"> <pre className="p-3 bg-body-tertiary text-body rounded border">
{JSON.stringify(queryResult, null, 2)} {JSON.stringify(queryResult, null, 2)}
</pre> </pre>
)} )}