feat: Lock webchat to irc.supernets.org with simplified connect form

This commit configures Hard Lounge as a dedicated webchat client for
SuperNETs IRC, requiring only a nickname to connect.

- Set `public: true` to enable public mode (no user accounts required)
- Set `lockNetwork: true` to lock connections to irc.supernets.org only

Users will automatically connect to irc.supernets.org:6697 (TLS) and
join #superbowl upon entering a nickname.

- Added simplified connect form for public + lockNetwork mode
- Form now shows only the nickname field when both settings are enabled
- Hidden fields: server, port, TLS, username, realname, channels,
  leave message, authentication options
- Added CSS styling for proper spacing on simplified form

- Pinned to Node 20 Alpine (from lts-alpine) for compatibility
- Added py3-setuptools to fix distutils module error with Python 3.12
- Fixed file ownership with --chown=node:node on COPY commands
- Moved USER node directive after COPY to fix permission issues
- Pre-create /var/opt/hardlounge directory with correct ownership
This commit is contained in:
2025-12-29 18:36:50 -08:00
parent 8cc5eed920
commit 13164b89aa
3 changed files with 143 additions and 111 deletions

View File

@@ -1,18 +1,20 @@
FROM node:lts-alpine
RUN apk add --no-cache --virtual=build-dependencies build-base git python3-dev && \
FROM node:20-alpine
RUN apk add --no-cache --virtual=build-dependencies build-base git python3-dev py3-setuptools && \
apk add --no-cache yarn
USER node
WORKDIR /var/opt/hardlounge-src
ENV THELOUNGE_HOME /var/opt/hardlounge
COPY package.json yarn.lock .
COPY --chown=node:node package.json yarn.lock .
USER node
RUN yarn install
COPY . .
COPY --chown=node:node . .
RUN NODE_ENV=production yarn build && \
yarn link && \
yarn --non-interactive cache clean && \
ln -s /var/opt/hardlounge-src/index.js /var/opt/hardlounge-src/hardlounge
USER root
RUN apk del --purge build-dependencies
RUN apk del --purge build-dependencies && \
mkdir -p /var/opt/hardlounge && \
chown -R node:node /var/opt/hardlounge
USER node
EXPOSE 9000
CMD ["/var/opt/hardlounge-src/hardlounge", "start"]

View File

@@ -206,122 +206,143 @@
</div>
</template>
<h2>User preferences</h2>
<div class="connect-row">
<label for="connect:nick">Nick</label>
<input
id="connect:nick"
v-model="defaults.nick"
class="input nick"
name="nick"
pattern="[^\s:!@]+"
maxlength="100"
required
@input="onNickChanged"
/>
</div>
<template v-if="!config?.useHexIp">
<div class="connect-row">
<label for="connect:username">Username</label>
<!-- Simplified form for public + lockNetwork mode: only show nick -->
<template v-if="config?.lockNetwork && store.state.serverConfiguration?.public">
<div class="connect-row simple-nick">
<label for="connect:nick">Nick</label>
<input
id="connect:username"
ref="usernameInput"
v-model.trim="defaults.username"
class="input username"
name="username"
id="connect:nick"
v-model="defaults.nick"
class="input nick"
name="nick"
pattern="[^\s:!@]+"
maxlength="100"
required
@input="onNickChanged"
/>
</div>
</template>
<div class="connect-row">
<label for="connect:realname">Real name</label>
<input
id="connect:realname"
v-model.trim="defaults.realname"
class="input"
name="realname"
maxlength="300"
/>
</div>
<div class="connect-row">
<label for="connect:leaveMessage">Leave message</label>
<input
id="connect:leaveMessage"
v-model.trim="defaults.leaveMessage"
autocomplete="off"
class="input"
name="leaveMessage"
placeholder="Hard Lounge - https://git.supernets.org/supernets/hardlounge"
/>
</div>
<template v-if="defaults.uuid && !store.state.serverConfiguration?.public">
<!-- Full form for other modes -->
<template v-else>
<h2>User preferences</h2>
<div class="connect-row">
<label for="connect:commands">
Commands
<span
class="tooltipped tooltipped-ne tooltipped-no-delay"
aria-label="One /command per line.
Each command will be executed in
the server tab on new connection"
>
<button class="extra-help" />
</span>
</label>
<textarea
id="connect:commands"
ref="commandsInput"
autocomplete="off"
:value="defaults.commands ? defaults.commands.join('\n') : ''"
class="input"
name="commands"
@input="resizeCommandsInput"
/>
</div>
</template>
<template v-else-if="!defaults.uuid">
<div class="connect-row">
<label for="connect:channels">Channels</label>
<label for="connect:nick">Nick</label>
<input
id="connect:channels"
v-model.trim="defaults.join"
class="input"
name="join"
id="connect:nick"
v-model="defaults.nick"
class="input nick"
name="nick"
pattern="[^\s:!@]+"
maxlength="100"
required
@input="onNickChanged"
/>
</div>
</template>
<template v-if="store.state.serverConfiguration?.public">
<template v-if="config?.lockNetwork">
<template v-if="!config?.useHexIp">
<div class="connect-row">
<label></label>
<div class="input-wrap">
<label class="tls">
<input v-model="displayPasswordField" type="checkbox" />
I have a password
</label>
</div>
</div>
<div v-if="displayPasswordField" class="connect-row">
<label for="connect:password">Password</label>
<RevealPassword
v-slot:default="slotProps"
class="input-wrap password-container"
>
<input
id="connect:password"
ref="publicPassword"
v-model="defaults.password"
class="input"
:type="slotProps.isVisible ? 'text' : 'password'"
placeholder="Server password (optional)"
name="password"
maxlength="300"
/>
</RevealPassword>
<label for="connect:username">Username</label>
<input
id="connect:username"
ref="usernameInput"
v-model.trim="defaults.username"
class="input username"
name="username"
maxlength="100"
/>
</div>
</template>
<div class="connect-row">
<label for="connect:realname">Real name</label>
<input
id="connect:realname"
v-model.trim="defaults.realname"
class="input"
name="realname"
maxlength="300"
/>
</div>
<div class="connect-row">
<label for="connect:leaveMessage">Leave message</label>
<input
id="connect:leaveMessage"
v-model.trim="defaults.leaveMessage"
autocomplete="off"
class="input"
name="leaveMessage"
placeholder="Hard Lounge - https://git.supernets.org/supernets/hardlounge"
/>
</div>
<template v-if="defaults.uuid && !store.state.serverConfiguration?.public">
<div class="connect-row">
<label for="connect:commands">
Commands
<span
class="tooltipped tooltipped-ne tooltipped-no-delay"
aria-label="One /command per line.
Each command will be executed in
the server tab on new connection"
>
<button class="extra-help" />
</span>
</label>
<textarea
id="connect:commands"
ref="commandsInput"
autocomplete="off"
:value="defaults.commands ? defaults.commands.join('\n') : ''"
class="input"
name="commands"
@input="resizeCommandsInput"
/>
</div>
</template>
<template v-else-if="!defaults.uuid">
<div class="connect-row">
<label for="connect:channels">Channels</label>
<input
id="connect:channels"
v-model.trim="defaults.join"
class="input"
name="join"
/>
</div>
</template>
<template v-if="store.state.serverConfiguration?.public">
<template v-if="config?.lockNetwork">
<div class="connect-row">
<label></label>
<div class="input-wrap">
<label class="tls">
<input v-model="displayPasswordField" type="checkbox" />
I have a password
</label>
</div>
</div>
<div v-if="displayPasswordField" class="connect-row">
<label for="connect:password">Password</label>
<RevealPassword
v-slot:default="slotProps"
class="input-wrap password-container"
>
<input
id="connect:password"
ref="publicPassword"
v-model="defaults.password"
class="input"
:type="slotProps.isVisible ? 'text' : 'password'"
placeholder="Server password (optional)"
name="password"
maxlength="300"
/>
</RevealPassword>
</div>
</template>
</template>
</template>
<template v-else>
<!-- Authentication section only for private mode -->
<template v-if="!store.state.serverConfiguration?.public && !(config?.lockNetwork && store.state.serverConfiguration?.public)">
<h2 id="label-auth">Authentication</h2>
<div class="connect-row connect-auth" role="group" aria-labelledby="label-auth">
<label class="opt">
@@ -435,6 +456,15 @@ the server tab on new connection"
margin: 0;
user-select: text;
}
/* Simplified connect form styling */
#connect .connect-row.simple-nick {
margin-top: 20px;
}
#connect .connect-row.simple-nick label {
margin-bottom: 8px;
}
</style>
<script lang="ts">

View File

@@ -16,7 +16,7 @@ module.exports = {
// channels and scrollbacks are available when they come back.
//
// This value is set to `false` by default.
public: false,
public: true,
// ### `host`
//
@@ -287,7 +287,7 @@ module.exports = {
// These fields will also be hidden from the UI.
//
// This value is set to `false` by default.
lockNetwork: false,
lockNetwork: true,
// ## User management