UI improvements (#340)

* fix: fix syntax in Dockerfile

* feat: UI improvements

hide signup button when disabled in settings

* feat: add setting hide_allowed_ip_input

allow to disable the ip restriction per settings

* fix remove unused translation

* feat: hide account when not logged in

hide account menu item if user is not logged in

* Fix quill editor to not disapper after change
This commit is contained in:
Valentin Brandner 2025-02-07 08:38:41 +01:00 committed by GitHub
parent 6dc899c9bd
commit 41a7ee8121
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 124 additions and 76 deletions

2
.gitignore vendored
View File

@ -124,3 +124,5 @@ data/
.vscode
hemmelig.backup.db
client/build/
.idea

View File

@ -23,7 +23,7 @@ RUN npm run build
# Get ready for step two of the docker image build
FROM node:20-alpine
RUN apk add curl
RUN apk add curl openssl --no-cache
WORKDIR /home/node/hemmelig
@ -33,9 +33,10 @@ COPY package*.json ./
RUN npm ci --production --ignore-scripts
RUN chown -R node.node ./
RUN chown -R node:node ./
COPY . .
RUN rm -f *.mp4 *.gif
RUN npx prisma generate

View File

@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, Navigate } from 'react-router-dom';
import { IconFingerprint, IconList, IconLockOff, IconLogin, IconUser } from '@tabler/icons';
import { refresh } from '../../api/authentication.js';
import { getCookie, refreshCookie } from '../../helpers/cookie';
import useAuthStore from '../../stores/authStore';
@ -170,32 +169,4 @@ const Header = () => {
);
};
const NavLinks = ({ mobile, onClick }) => {
const { t } = useTranslation();
const { isLoggedIn } = useAuthStore();
const links = [
!isLoggedIn && { label: t('sign_up'), icon: IconUser, to: '/signup' },
!isLoggedIn && { label: t('sign_in'), icon: IconLogin, to: '/signin' },
isLoggedIn && { label: t('sign_out'), icon: IconLockOff, to: '/signout' },
{ label: t('account.home.title'), icon: IconUser, to: '/account' },
{ label: t('public_list'), icon: IconList, to: '/public' },
{ label: t('privacy.title'), icon: IconFingerprint, to: '/privacy' },
].filter(Boolean);
return links.map((link) => (
<Link
key={link.to}
to={link.to}
onClick={onClick}
className={`flex items-center gap-2 px-4 py-2 text-gray-300 rounded-md
hover:text-white hover:bg-gray-800 transition-colors duration-200
${mobile ? 'w-full' : ''}`}
>
<link.icon size={16} className="text-gray-400" />
<span className="text-sm font-medium">{link.label}</span>
</Link>
));
};
export default Header;

View File

@ -1,19 +1,24 @@
import { IconFingerprint, IconList, IconLockOff, IconLogin, IconUser } from '@tabler/icons';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import useSettingsStore from '../../stores/settingsStore.js';
const Nav = ({ opened, toggle, isLoggedIn }) => {
const { t } = useTranslation();
const { settings } = useSettingsStore();
const navItems = [];
if (!isLoggedIn) {
const hideSignUp =
isLoggedIn || settings.disable_user_account_creation || settings.disable_users;
if (!hideSignUp) {
navItems.push({
label: t('sign_up'),
icon: <IconUser size="1rem" stroke={1.5} />,
to: '/signup',
});
}
if (!isLoggedIn) {
navItems.push({
label: t('sign_in'),
icon: <IconLogin size="1rem" stroke={1.5} />,
@ -27,14 +32,14 @@ const Nav = ({ opened, toggle, isLoggedIn }) => {
icon: <IconLockOff size="1rem" stroke={1.5} />,
to: '/signout',
});
}
navItems.push(
{
navItems.push({
label: t('account.home.title'),
icon: <IconUser size="1rem" stroke={1.5} />,
to: '/account',
},
});
}
navItems.push(
{
label: t('public_list'),
icon: <IconList size="1rem" stroke={1.5} />,

View File

@ -2,6 +2,14 @@ import { useRef } from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
// https://github.com/zenoamaro/react-quill/issues/836#issuecomment-2440290893
class ReactQuillFixed extends ReactQuill {
destroyEditor() {
super.destroyEditor();
delete this.editor;
}
}
const Quill = ({ value, onChange, readOnly, defaultValue }) => {
const quillRef = useRef(null);
// Define modules based on readOnly state
@ -21,7 +29,7 @@ const Quill = ({ value, onChange, readOnly, defaultValue }) => {
return (
<div className="bg-gray-800 border border-gray-700 rounded-md overflow-hidden">
<ReactQuill
<ReactQuillFixed
ref={quillRef}
value={value || ''}
onChange={onChange}

View File

@ -142,6 +142,31 @@ const Settings = () => {
</div>
</div>
{/* Disable allowed IP restriction */}
<div className="flex items-start space-x-3">
<input
type="checkbox"
id="hide_allowed_ip_input"
checked={formData.hide_allowed_ip_input}
onChange={(e) =>
setFormData({ ...formData, hide_allowed_ip_input: e.target.checked })
}
className="mt-1 rounded border-gray-700 bg-gray-800 text-blue-500
focus:ring-blue-500 focus:ring-offset-gray-900"
/>
<div>
<label
htmlFor="hide_allowed_ip_input"
className="block text-sm font-medium text-gray-200"
>
{t('account.settings.hide_allowed_ip_input')}
</label>
<p className="text-sm text-gray-400">
{t('account.settings.hide_allowed_ip_input_description')}
</p>
</div>
</div>
{/* Disable File Upload */}
<div className="flex items-start space-x-3">
<input

View File

@ -455,6 +455,7 @@ const ApiDocs = () => {
{
disable_users: 'required boolean',
disable_user_account_creation: 'required boolean',
hide_allowed_ip_input: 'optional boolean',
read_only: 'required boolean',
disable_file_upload: 'required boolean',
restrict_organization_email: 'optional string',

View File

@ -157,7 +157,7 @@ const Home = () => {
value={formData.title}
onChange={(e) => setField('formData.title', e.target.value)}
readOnly={inputReadOnly}
className="w-full pl-10 pr-3 py-2.5 bg-gray-800 border border-gray-700 rounded-md
className="w-full pl-10 pr-3 py-2.5 bg-gray-800 border border-gray-700 rounded-md
focus:ring-2 focus:ring-hemmelig focus:border-transparent
text-base text-gray-100 placeholder-gray-500"
/>
@ -229,7 +229,7 @@ const Home = () => {
setField('formData.password', e.target.value)
}
readOnly={inputReadOnly}
className="w-full pl-10 pr-10 bg-gray-800 border border-gray-700
className="w-full pl-10 pr-10 bg-gray-800 border border-gray-700
rounded-lg text-gray-100 placeholder-gray-500
focus:border-primary focus:ring-1 focus:ring-primary"
placeholder={t('home.password')}
@ -240,25 +240,29 @@ const Home = () => {
</div>
)}
<div className="relative">
<div className="absolute left-3 top-[13px] text-gray-400 pointer-events-none">
<IconNetwork size={18} />
{!settings.hide_allowed_ip_input && (
<div className="relative">
<div className="absolute left-3 top-[13px] text-gray-400 pointer-events-none">
<IconNetwork size={18} />
</div>
<input
type="text"
name="allowedIp"
placeholder="0.0.0.0/0"
value={formData.allowedIp}
onChange={(e) =>
setField('formData.allowedIp', e.target.value)
}
readOnly={inputReadOnly}
className="w-full pl-10 pr-3 py-2.5 bg-gray-800 border border-gray-700 rounded-md
focus:ring-2 focus:ring-hemmelig focus:border-transparent
text-base text-gray-100 placeholder-gray-500"
/>
<p className="mt-2 text-xs text-gray-400">
{t('home.restrict_from_ip')}
</p>
</div>
<input
type="text"
name="allowedIp"
placeholder="0.0.0.0/0"
value={formData.allowedIp}
onChange={(e) => setField('formData.allowedIp', e.target.value)}
readOnly={inputReadOnly}
className="w-full pl-10 pr-3 py-2.5 bg-gray-800 border border-gray-700 rounded-md
focus:ring-2 focus:ring-hemmelig focus:border-transparent
text-base text-gray-100 placeholder-gray-500"
/>
<p className="mt-2 text-xs text-gray-400">
{t('home.restrict_from_ip')}
</p>
</div>
)}
</div>
<div className="space-y-4">
@ -284,24 +288,28 @@ const Home = () => {
</p>
</div>
<div className="relative">
<div className="absolute left-3 top-[13px] text-gray-400 pointer-events-none">
<IconEye size={18} />
{!formData.preventBurn && (
<div className="relative">
<div className="absolute left-3 top-[13px] text-gray-400 pointer-events-none">
<IconEye size={18} />
</div>
<input
type="number"
name="maxViews"
value={formData.maxViews}
onChange={(e) =>
setField('formData.maxViews', e.target.value)
}
min="1"
max="999"
className="w-full pl-10 pr-3 py-2.5 bg-gray-800 border border-gray-700 rounded-md
text-gray-100 focus:border-primary focus:ring-1 focus:ring-primary"
/>
<p className="mt-2 text-xs text-gray-400">
{t('home.max_views_description')}
</p>
</div>
<input
type="number"
name="maxViews"
value={formData.maxViews}
onChange={(e) => setField('formData.maxViews', e.target.value)}
min="1"
max="999"
className="w-full pl-10 pr-3 py-2.5 bg-gray-800 border border-gray-700 rounded-md
text-gray-100 focus:border-primary focus:ring-1 focus:ring-primary"
/>
<p className="mt-2 text-xs text-gray-400">
{t('home.max_views_description')}
</p>
</div>
)}
<div className="flex items-center justify-between p-3 bg-black/20 rounded-lg border border-white/[0.08]">
<div className="flex items-center space-x-3">

View File

@ -7,6 +7,7 @@ const useSettingsStore = create((set) => ({
read_only: false,
disable_file_upload: false,
restrict_organization_email: '',
hide_allowed_ip_input: true,
},
isLoading: true,
error: null,

View File

@ -0,0 +1,16 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Settings" (
"id" TEXT NOT NULL PRIMARY KEY,
"disable_users" BOOLEAN NOT NULL DEFAULT false,
"disable_user_account_creation" BOOLEAN NOT NULL DEFAULT false,
"read_only" BOOLEAN NOT NULL DEFAULT false,
"disable_file_upload" BOOLEAN NOT NULL DEFAULT false,
"hide_allowed_ip_input" BOOLEAN NOT NULL DEFAULT false,
"restrict_organization_email" TEXT NOT NULL DEFAULT ''
);
INSERT INTO "new_Settings" ("disable_file_upload", "disable_user_account_creation", "disable_users", "id", "read_only", "restrict_organization_email") SELECT "disable_file_upload", "disable_user_account_creation", "disable_users", "id", "read_only", "restrict_organization_email" FROM "Settings";
DROP TABLE "Settings";
ALTER TABLE "new_Settings" RENAME TO "Settings";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -61,5 +61,6 @@ model Settings {
disable_user_account_creation Boolean @default(false)
read_only Boolean @default(false)
disable_file_upload Boolean @default(false)
hide_allowed_ip_input Boolean @default(false)
restrict_organization_email String @default("")
}

View File

@ -54,6 +54,8 @@
"disable_user_account_creation_description": "Benutzern nicht erlauben, ein neues Konto zu erstellen. Als Administrator können Sie weiterhin neue Benutzerkonten erstellen",
"disable_file_upload": "Dateiupload deaktivieren",
"disable_file_upload_description": "Deaktivieren Sie den Dateiupload für Ihre Instanz",
"hide_allowed_ip_input": "Beschränkung auf IPs deaktivieren",
"hide_allowed_ip_input_description": "Eingabefeld für die Beschränkung auf erlaubte IP-Adressen ausblenden",
"restrict_organization_email": "E-Mail-Domäne einschränken",
"restrict_organization_email_description": "Dies wird die Benutzerregistrierung für eine bestimmte E-Mail-Domäne einschränken"
},

View File

@ -53,6 +53,8 @@
"disable_signin": "Should user sign in be disabled?",
"disable_user_account_creation": "Disable user account creation",
"disable_user_account_creation_description": "Do not allow users to create a new account. As an admin, you will still be able to create new user accounts",
"hide_allowed_ip_input": "Disable IP restriction",
"hide_allowed_ip_input_description": "Hide the allowed IPs input field in the secret creation form",
"disable_file_upload": "Disable file upload",
"disable_file_upload_description": "Disable file upload for your instance",
"restrict_organization_email": "Restrict to email domain",

View File

@ -18,6 +18,7 @@ async function settings(fastify) {
id: 'admin_settings',
disable_users: false,
disable_user_account_creation: false,
hide_allowed_ip_input: false,
read_only: false,
disable_file_upload: false,
restrict_organization_email: '',
@ -42,6 +43,7 @@ async function settings(fastify) {
properties: {
disable_users: { type: 'boolean', default: false },
disable_user_account_creation: { type: 'boolean', default: false },
hide_allowed_ip_input: { type: 'boolean', default: false },
read_only: { type: 'boolean', default: false },
disable_file_upload: { type: 'boolean', default: false },
restrict_organization_email: { type: 'string', default: '' },
@ -60,6 +62,7 @@ async function settings(fastify) {
const {
disable_users,
disable_user_account_creation,
hide_allowed_ip_input,
read_only,
disable_file_upload,
restrict_organization_email,
@ -72,6 +75,7 @@ async function settings(fastify) {
update: {
disable_users,
disable_user_account_creation,
hide_allowed_ip_input,
read_only,
disable_file_upload,
restrict_organization_email: getEmailDomain(restrict_organization_email),
@ -80,6 +84,7 @@ async function settings(fastify) {
id: 'admin_settings',
disable_users,
disable_user_account_creation,
hide_allowed_ip_input,
read_only,
disable_file_upload,
restrict_organization_email: getEmailDomain(restrict_organization_email),