Compare commits
8 Commits
upstream-p
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a31eb08ceb | |||
| b47fa524c3 | |||
| ecb9fe6b33 | |||
| fc2190c7cd | |||
| 8937658597 | |||
| 6cd5f0fa81 | |||
| 13164b89aa | |||
| 8cc5eed920 |
19
Dockerfile
19
Dockerfile
@@ -1,18 +1,21 @@
|
||||
FROM node:lts-alpine
|
||||
RUN apk add --no-cache --virtual=build-dependencies build-base git python3-dev && \
|
||||
apk add --no-cache yarn
|
||||
USER node
|
||||
FROM node:20-alpine
|
||||
RUN apk add --no-cache --virtual=build-dependencies build-base git python3-dev py3-setuptools && \
|
||||
apk add --no-cache yarn su-exec
|
||||
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
|
||||
USER node
|
||||
RUN apk del --purge build-dependencies && \
|
||||
mkdir -p /var/opt/hardlounge/logs /var/opt/hardlounge/users /var/opt/hardlounge/packages && \
|
||||
chown -R node:node /var/opt/hardlounge
|
||||
COPY --chmod=755 docker-entrypoint.sh /docker-entrypoint.sh
|
||||
EXPOSE 9000
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["/var/opt/hardlounge-src/hardlounge", "start"]
|
||||
@@ -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">
|
||||
|
||||
@@ -30,22 +30,6 @@
|
||||
<NetworkList />
|
||||
</div>
|
||||
<footer id="footer">
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||
aria-label="Connect to network"
|
||||
><router-link
|
||||
v-slot:default="{navigate, isActive}"
|
||||
to="/connect"
|
||||
role="tab"
|
||||
aria-controls="connect"
|
||||
>
|
||||
<button
|
||||
:class="['icon', 'connect', {active: isActive}]"
|
||||
:aria-selected="isActive"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
/> </router-link
|
||||
></span>
|
||||
<span class="tooltipped tooltipped-n tooltipped-no-touch" aria-label="Settings"
|
||||
><router-link
|
||||
v-slot:default="{navigate, isActive}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ services:
|
||||
hardlounge:
|
||||
image: git.supernets.org/supernets/hardlounge:latest
|
||||
build: .
|
||||
ports:
|
||||
- "9000:9000"
|
||||
user: "${UID}:${GID}"
|
||||
network_mode: host
|
||||
volumes:
|
||||
- "$PWD/config:/var/opt/hardlounge"
|
||||
- ./data/logs:/var/opt/hardlounge/logs
|
||||
- ./data/packages:/var/opt/hardlounge/packages
|
||||
- ./config:/var/opt/hardlounge
|
||||
|
||||
19
docker-entrypoint.sh
Normal file
19
docker-entrypoint.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Fix permissions on mounted volume at runtime
|
||||
chown -R node:node /var/opt/hardlounge 2>/dev/null || true
|
||||
chmod -R 755 /var/opt/hardlounge 2>/dev/null || true
|
||||
|
||||
# Create required subdirectories
|
||||
mkdir -p /var/opt/hardlounge/logs /var/opt/hardlounge/users /var/opt/hardlounge/packages 2>/dev/null || true
|
||||
chown -R node:node /var/opt/hardlounge 2>/dev/null || true
|
||||
|
||||
# Copy default config if it doesn't exist
|
||||
if [ ! -f /var/opt/hardlounge/config.js ]; then
|
||||
cp /var/opt/hardlounge-src/dist/defaults/config.js /var/opt/hardlounge/config.js 2>/dev/null || true
|
||||
chown node:node /var/opt/hardlounge/config.js 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Run as node user
|
||||
exec su-exec node "$@"
|
||||
@@ -61,27 +61,32 @@ if (!Config.values.public) {
|
||||
program.parse(argvWithoutOptions.operands.concat(argvWithoutOptions.unknown));
|
||||
|
||||
function createPackagesFolder() {
|
||||
const packagesPath = Config.getPackagesPath();
|
||||
const packagesConfig = path.join(packagesPath, "package.json");
|
||||
try {
|
||||
const packagesPath = Config.getPackagesPath();
|
||||
const packagesConfig = path.join(packagesPath, "package.json");
|
||||
|
||||
// Create node_modules folder, otherwise yarn will start walking upwards to find one
|
||||
fs.mkdirSync(path.join(packagesPath, "node_modules"), { recursive: true });
|
||||
// Create node_modules folder, otherwise yarn will start walking upwards to find one
|
||||
fs.mkdirSync(path.join(packagesPath, "node_modules"), { recursive: true });
|
||||
|
||||
// Create package.json with private set to true, if it doesn't exist already
|
||||
if (!fs.existsSync(packagesConfig)) {
|
||||
fs.writeFileSync(
|
||||
packagesConfig,
|
||||
JSON.stringify(
|
||||
{
|
||||
private: true,
|
||||
description:
|
||||
"Packages for Hard Lounge. Use `thelounge install <package>` command to add a package.",
|
||||
dependencies: {},
|
||||
},
|
||||
null,
|
||||
"\t"
|
||||
)
|
||||
);
|
||||
// Create package.json with private set to true, if it doesn't exist already
|
||||
if (!fs.existsSync(packagesConfig)) {
|
||||
fs.writeFileSync(
|
||||
packagesConfig,
|
||||
JSON.stringify(
|
||||
{
|
||||
private: true,
|
||||
description:
|
||||
"Packages for Hard Lounge. Use `thelounge install <package>` command to add a package.",
|
||||
dependencies: {},
|
||||
},
|
||||
null,
|
||||
"\t"
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore permission errors on mounted volumes
|
||||
log.warn("Unable to create packages folder, package management may not work");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,16 +22,38 @@ program
|
||||
|
||||
function initalizeConfig() {
|
||||
if (!fs.existsSync(Config.getConfigPath())) {
|
||||
fs.mkdirSync(Config.getHomePath(), {recursive: true});
|
||||
fs.chmodSync(Config.getHomePath(), "0700");
|
||||
fs.copyFileSync(
|
||||
path.resolve(path.join(__dirname, "..", "..", "defaults", "config.js")),
|
||||
Config.getConfigPath()
|
||||
);
|
||||
log.info(`Configuration file created at ${colors.green(Config.getConfigPath())}.`);
|
||||
try {
|
||||
fs.mkdirSync(Config.getHomePath(), {recursive: true});
|
||||
|
||||
try {
|
||||
fs.chmodSync(Config.getHomePath(), "0700");
|
||||
} catch (e) {
|
||||
// Ignore chmod errors (e.g., on mounted volumes)
|
||||
}
|
||||
|
||||
fs.copyFileSync(
|
||||
path.resolve(path.join(__dirname, "..", "..", "defaults", "config.js")),
|
||||
Config.getConfigPath()
|
||||
);
|
||||
log.info(`Configuration file created at ${colors.green(Config.getConfigPath())}.`);
|
||||
} catch (e) {
|
||||
log.error(
|
||||
"Unable to create config directory/file. Please ensure the config volume is writable or pre-create config.js"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(Config.getUsersPath(), {recursive: true, mode: 0o700});
|
||||
try {
|
||||
fs.mkdirSync(Config.getUsersPath(), {recursive: true, mode: 0o700});
|
||||
} catch (e) {
|
||||
// Ignore permission errors on mounted volumes
|
||||
try {
|
||||
fs.mkdirSync(Config.getUsersPath(), {recursive: true});
|
||||
} catch (e2) {
|
||||
// Ignore if this also fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default program;
|
||||
|
||||
@@ -276,7 +276,7 @@ class Config {
|
||||
try {
|
||||
fs.mkdirSync(userLogsPath, {recursive: true, mode: 0o750});
|
||||
} catch (e: any) {
|
||||
log.error("Unable to create logs directory", e);
|
||||
log.warn("Unable to create logs directory, logging to disk may not work");
|
||||
}
|
||||
} else if (logsStat && logsStat.mode & 0o001) {
|
||||
log.warn(
|
||||
|
||||
Reference in New Issue
Block a user