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:
parent
6dc899c9bd
commit
41a7ee8121
2
.gitignore
vendored
2
.gitignore
vendored
@ -124,3 +124,5 @@ data/
|
||||
.vscode
|
||||
hemmelig.backup.db
|
||||
client/build/
|
||||
|
||||
.idea
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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} />,
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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">
|
||||
|
@ -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,
|
||||
|
@ -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;
|
@ -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("")
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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),
|
||||
|
Loading…
x
Reference in New Issue
Block a user