Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 46 additions & 16 deletions bin/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@ import { init as initAPI } from '@nictool/api/routes/index.js'

const execFileAsync = promisify(execFile)

// ---------------------------------------------------------------------------
// Prevent stray TLS errors (e.g. plain HTTP hitting the HTTPS port) from
// crashing the process. Only EPROTO is swallowed; everything else exits.
// ---------------------------------------------------------------------------
process.on('uncaughtException', (err) => {
if (err.code === 'EPROTO') {
console.error(`[uncaughtException] TLS error (ignored): ${err.message}`)
return
}
console.error('[uncaughtException] Fatal:', err)
process.exit(1)
})

process.on('unhandledRejection', (reason) => {
if (reason?.code === 'EPROTO') {
console.error(`[unhandledRejection] TLS error (ignored): ${reason.message}`)
return
}
console.error('[unhandledRejection] Fatal:', reason)
process.exit(1)
})

// ---------------------------------------------------------------------------
// CLI arguments
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -55,23 +77,29 @@ try {
}

// ---------------------------------------------------------------------------
// TLS – discover existing certs or generate a self-signed one
// TLS – skip entirely in development mode, otherwise discover or generate
// ---------------------------------------------------------------------------

const useTLS = (process.env.NICTOOL_TLS ?? 'auto') !== 'false'
const osHostname = os.hostname()
const tlsDir = path.join(configDir, 'etc', 'tls')

const discovered = await discoverTLS(tlsDir, osHostname)
let tls, host
let tls = null
let host = osHostname

if (discovered) {
const { hostname: certHost, ...pemMaterial } = discovered
tls = pemMaterial
host = certHost
if (!useTLS) {
console.log('TLS disabled (NICTOOL_TLS=false) — running plain HTTP')
host = 'localhost'
} else {
console.log(`Generating self-signed cert for ${osHostname}`)
tls = await generateTLS(tlsDir, osHostname)
host = osHostname
const tlsDir = path.join(configDir, 'etc', 'tls')
const discovered = await discoverTLS(tlsDir, osHostname)

if (discovered) {
const { hostname: certHost, ...pemMaterial } = discovered
tls = pemMaterial
host = certHost
} else {
console.log(`Generating self-signed cert for ${osHostname}`)
tls = await generateTLS(tlsDir, osHostname)
}
}

// ---------------------------------------------------------------------------
Expand All @@ -82,10 +110,12 @@ const tomlPath = path.join(configDir, 'etc', 'nictool.toml')
const nicConfig = await readNicToolToml(tomlPath)

// ---------------------------------------------------------------------------
// Port selection – prefer 443, fall back to 8443
// Port selection – HTTP prefers 8080, HTTPS prefers 443/8443
// ---------------------------------------------------------------------------

const port = (await resolvePort(host, 443)) ?? (await resolvePort(host, 8443)) ?? (await randomAvailablePort(host))
const port = useTLS
? ((await resolvePort(host, 443)) ?? (await resolvePort(host, 8443)) ?? (await randomAvailablePort(host)))
: ((await resolvePort(host, 8080)) ?? (await resolvePort(host, 80)) ?? (await randomAvailablePort(host)))

// ---------------------------------------------------------------------------
// If already configured, skip the configurator and go straight to services
Expand Down Expand Up @@ -237,9 +267,9 @@ function storeTypeToEnv(type) {
*/
function buildRemoteUrl(config) {
if (!config?.api || config.api.mode !== 'remote') return null
const { host, port } = config.api
const { host, port, scheme: configScheme } = config.api
if (!host || !port) return null
const scheme = /^(localhost|127\.|::1)/.test(host) ? 'http' : 'https'
const scheme = configScheme ?? 'http'
return `${scheme}://${host}:${port}`
}

Expand Down
10 changes: 10 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM node:22-trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends openssl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package*.json .
RUN npm install --omit=dev
COPY . .
RUN chmod +x docker/entrypoint.sh \
&& ln -s node_modules/@nictool/api/conf.d conf.d
EXPOSE 8080 8443
ENTRYPOINT ["./docker/entrypoint.sh"]
27 changes: 27 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/sh
set -e

TOML="/data/etc/nictool.toml"
mkdir -p /data/etc

if [ ! -f "$TOML" ]; then
cat > "$TOML" <<EOF
configured = true

[store]
type = "${NICTOOL_STORE_TYPE:-mysql}"
host = "${NICTOOL_DB_HOST:-db}"
port = ${NICTOOL_DB_PORT:-3306}
user = "${NICTOOL_DB_USER:-nictool}"
password = "${NICTOOL_DB_USER_PASSWORD}"
database = "${NICTOOL_DB_NAME:-nictool}"

[api]
mode = "remote"
host = "${NICTOOL_API_HOST:-api}"
port = ${NICTOOL_API_PORT:-3000}
EOF
echo "Generated $TOML"
fi

exec node bin/start.js -c /data
81 changes: 81 additions & 0 deletions html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@
aria-selected="false"
>Users</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="tab-groups"
data-bs-toggle="tab"
data-bs-target="#groups_div"
type="button"
role="tab"
aria-controls="groups_div"
aria-selected="false"
>Groups</button>
</li>
</ul>

<ul class="navbar-nav gap-3">
Expand Down Expand Up @@ -292,6 +304,45 @@ <h3 class="text-center">Login</h3>
</div>
</div>
</div>

<div id="groups_div" class="tab-pane fade" role="tabpanel" aria-labelledby="tab-groups">
<div class="d-flex justify-content-end mb-1">
<button id="groupCreateBtn" type="button" class="btn btn-sm btn-outline-secondary">+ Create</button>
</div>
<table
id="group_table"
class="col-11 table table-striped table-hover table-bordered"
>
<thead>
<tr>
<th style="text-align: left">Name</th>
<th style="text-align: left">Parent</th>
<th style="text-align: center">Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="d-flex gap-3 mt-3 text-body-secondary small flex-wrap">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="groupShowDeleted"
/>
<label class="form-check-label" for="groupShowDeleted">Show deleted</label>
</div>
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="groupShowSubgroups"
/>
<label class="form-check-label" for="groupShowSubgroups">Subgroups</label>
</div>
</div>
</div>
</div>
</div>
</main>
Expand Down Expand Up @@ -673,5 +724,35 @@ <h5 class="offcanvas-title" id="nsEditPaneLabel">Nameserver</h5>
</div>
</div>

<!-- Group create/edit pane -->
<div
class="offcanvas offcanvas-end"
tabindex="-1"
id="groupEditPane"
aria-labelledby="groupEditPaneLabel"
style="width: 380px;"
>
<div class="offcanvas-header border-bottom">
<h5 class="offcanvas-title" id="groupEditPaneLabel">Group</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="form-floating mb-3">
<input type="text" class="form-control" id="groupEditName" placeholder="My Group" />
<label for="groupEditName">Name</label>
</div>
<div class="form-floating mb-3">
<select class="form-select" id="groupEditParent">
<option value="">-- Root --</option>
</select>
<label for="groupEditParent">Parent Group</label>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<button type="button" class="btn btn-secondary" data-bs-dismiss="offcanvas">Cancel</button>
<button type="button" class="btn btn-primary" id="groupEditSaveBtn">Save</button>
</div>
</div>
</div>

</body>
</html>
Loading
Loading