mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 15:35:55 +01:00
✨ Add feature for edit mode password
This commit is contained in:
@@ -9,10 +9,10 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
HoverCard,
|
HoverCard,
|
||||||
Modal,
|
Modal,
|
||||||
Stack,
|
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconAnchor,
|
IconAnchor,
|
||||||
@@ -35,6 +35,7 @@ import { useConfigContext } from '../../../../config/provider';
|
|||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
|
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
|
||||||
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
|
||||||
|
import { useColorTheme } from '../../../../tools/color';
|
||||||
import { usePrimaryGradient } from '../../../layout/useGradient';
|
import { usePrimaryGradient } from '../../../layout/useGradient';
|
||||||
import Credits from '../../../Settings/Common/Credits';
|
import Credits from '../../../Settings/Common/Credits';
|
||||||
|
|
||||||
@@ -161,9 +162,9 @@ interface ExtendedInitOptions extends InitOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
const useInformationTableItems = (newVersionAvailable?: string): InformationTableItem[] => {
|
||||||
const colorGradiant = usePrimaryGradient();
|
|
||||||
const { attributes } = usePackageAttributesStore();
|
const { attributes } = usePackageAttributesStore();
|
||||||
const { editModeEnabled } = useEditModeInformationStore();
|
const { editModeEnabled } = useEditModeInformationStore();
|
||||||
|
const { primaryColor } = useColorTheme();
|
||||||
|
|
||||||
const { configVersion } = useConfigContext();
|
const { configVersion } = useConfigContext();
|
||||||
const { configs } = useConfigStore();
|
const { configs } = useConfigStore();
|
||||||
@@ -177,15 +178,19 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconKey size={20} />,
|
icon: <IconKey size={20} />,
|
||||||
label: 'experimental_disableEditMode',
|
label: 'experimental_disableEditMode',
|
||||||
content: (
|
content: (
|
||||||
<Stack>
|
<Tooltip
|
||||||
<Badge color="red">WARNING</Badge>
|
color="red"
|
||||||
<Text color="red" size="xs">
|
withinPortal
|
||||||
This is an experimental feature, where the edit mode is disabled entirely - no config
|
width={300}
|
||||||
|
multiline
|
||||||
|
withArrow
|
||||||
|
label="This is an experimental feature, where the edit mode is disabled entirely - no config
|
||||||
modifications are possbile anymore. All update requests for the config will be dropped
|
modifications are possbile anymore. All update requests for the config will be dropped
|
||||||
on the API. This will be removed in future versions, as Homarr will receive a proper
|
on the API. This will be removed in future versions, as Homarr will receive a proper
|
||||||
authentication system, which will make this obsolete.
|
authentication system, which will make this obsolete."
|
||||||
</Text>
|
>
|
||||||
</Stack>
|
<Badge color="red">WARNING</Badge>
|
||||||
|
</Tooltip>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -201,7 +206,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconLanguage size={20} />,
|
icon: <IconLanguage size={20} />,
|
||||||
label: 'i18n',
|
label: 'i18n',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{usedI18nNamespaces.length}
|
{usedI18nNamespaces.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
@@ -210,7 +215,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconVocabulary size={20} />,
|
icon: <IconVocabulary size={20} />,
|
||||||
label: 'locales',
|
label: 'locales',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{initOptions.locales.length}
|
{initOptions.locales.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
@@ -223,7 +228,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconSchema size={20} />,
|
icon: <IconSchema size={20} />,
|
||||||
label: 'configurationSchemaVersion',
|
label: 'configurationSchemaVersion',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{configVersion}
|
{configVersion}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
@@ -232,7 +237,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconFile size={20} />,
|
icon: <IconFile size={20} />,
|
||||||
label: 'configurationsCount',
|
label: 'configurationsCount',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{configs.length}
|
{configs.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
@@ -242,7 +247,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
label: 'version',
|
label: 'version',
|
||||||
content: (
|
content: (
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{attributes.packageVersion ?? 'Unknown'}
|
{attributes.packageVersion ?? 'Unknown'}
|
||||||
</Badge>
|
</Badge>
|
||||||
{newVersionAvailable && (
|
{newVersionAvailable && (
|
||||||
@@ -282,7 +287,7 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
|
|||||||
icon: <IconAnchor size={20} />,
|
icon: <IconAnchor size={20} />,
|
||||||
label: 'nodeEnvironment',
|
label: 'nodeEnvironment',
|
||||||
content: (
|
content: (
|
||||||
<Badge variant="gradient" gradient={colorGradiant}>
|
<Badge variant="light" color={primaryColor}>
|
||||||
{attributes.environment}
|
{attributes.environment}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { AboutModal } from '../../Dashboard/Modals/AboutModal/AboutModal';
|
|||||||
import { SettingsDrawer } from '../../Settings/SettingsDrawer';
|
import { SettingsDrawer } from '../../Settings/SettingsDrawer';
|
||||||
import { useCardStyles } from '../useCardStyles';
|
import { useCardStyles } from '../useCardStyles';
|
||||||
import { ColorSchemeSwitch } from './SettingsMenu/ColorSchemeSwitch';
|
import { ColorSchemeSwitch } from './SettingsMenu/ColorSchemeSwitch';
|
||||||
|
import { EditModeToggle } from './SettingsMenu/EditModeToggle';
|
||||||
|
|
||||||
export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
|
export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
|
||||||
const [drawerOpened, drawer] = useDisclosure(false);
|
const [drawerOpened, drawer] = useDisclosure(false);
|
||||||
@@ -25,6 +26,7 @@ export function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: str
|
|||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<ColorSchemeSwitch />
|
<ColorSchemeSwitch />
|
||||||
|
<EditModeToggle />
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
{!editModeEnabled && (
|
{!editModeEnabled && (
|
||||||
<Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}>
|
<Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}>
|
||||||
|
|||||||
78
src/components/layout/header/SettingsMenu/EditModeToggle.tsx
Normal file
78
src/components/layout/header/SettingsMenu/EditModeToggle.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { Button, Code, Menu, PasswordInput, Stack, Text } from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { openModal } from '@mantine/modals';
|
||||||
|
import { showNotification } from '@mantine/notifications';
|
||||||
|
import { IconEdit, IconEditOff } from '@tabler/icons';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useEditModeInformationStore } from '../../../../hooks/useEditModeInformation';
|
||||||
|
|
||||||
|
function ModalContent() {
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
triedPassword: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) => {
|
||||||
|
axios.post('/api/configs/tryToggleEdit', { tried: values.triedPassword }).then((res) => {
|
||||||
|
if (res.data.success) {
|
||||||
|
showNotification({
|
||||||
|
title: 'Success',
|
||||||
|
message: 'Successfully toggled edit mode, reloading the page...',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
title: 'Wrong password',
|
||||||
|
message: 'The password you entered is wrong.',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Text size="sm">
|
||||||
|
In order to toggle edit mode, you need to enter the password you entered in the
|
||||||
|
environment variable named <Code>EDIT_MODE_PASSWORD</Code>
|
||||||
|
</Text>
|
||||||
|
<PasswordInput
|
||||||
|
id="triedPassword"
|
||||||
|
label="Edit password"
|
||||||
|
autoFocus
|
||||||
|
required
|
||||||
|
{...form.getInputProps('triedPassword')}
|
||||||
|
/>
|
||||||
|
<Button type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditModeToggle() {
|
||||||
|
const { editModeEnabled } = useEditModeInformationStore();
|
||||||
|
const Icon = editModeEnabled ? IconEdit : IconEditOff;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
closeMenuOnClick={false}
|
||||||
|
icon={<Icon strokeWidth={1.2} size={18} />}
|
||||||
|
onClick={() =>
|
||||||
|
openModal({
|
||||||
|
title: 'Toggle edit mode',
|
||||||
|
centered: true,
|
||||||
|
size: 'lg',
|
||||||
|
children: <ModalContent />,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{editModeEnabled ? 'Enable edit mode' : 'Disable edit mode'}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@ export default function DockerMenuButton(props: any) {
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dockerEnabled) {
|
if (!dockerEnabled || process.env.DISABLE_EDIT_MODE === 'true') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
src/pages/api/configs/tryToggleEdit.tsx
Normal file
34
src/pages/api/configs/tryToggleEdit.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import Consola from 'consola';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
|
function Post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { tried } = req.body;
|
||||||
|
// Try to match the password with the EDIT_PASSWORD env variable
|
||||||
|
Consola.log(req.body, process.env.EDIT_MODE_PASSWORD);
|
||||||
|
if (tried === process.env.EDIT_MODE_PASSWORD) {
|
||||||
|
process.env.DISABLE_EDIT_MODE = process.env.DISABLE_EDIT_MODE === 'true' ? 'false' : 'true';
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Warn that there was a wrong password attempt (date : wrong password, person's IP)
|
||||||
|
Consola.warn(
|
||||||
|
`${new Date().toLocaleString()} : Wrong edit password attempt, from ${
|
||||||
|
req.headers['x-forwarded-for']
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return res.status(200).json({
|
||||||
|
success: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
// Filter out if the request is a POST or a GET
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
return Post(req, res);
|
||||||
|
}
|
||||||
|
return res.status(405).json({
|
||||||
|
statusCode: 405,
|
||||||
|
message: 'Method not allowed',
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user