Merge pull request #748 from ajnart/edit-mode-password

Edit mode password modal
This commit is contained in:
Thomas Camlong
2023-03-22 22:30:42 +08:00
committed by GitHub
8 changed files with 124 additions and 25 deletions

3
.gitignore vendored
View File

@@ -48,3 +48,6 @@ data/configs
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
#envfiles
.env

View File

@@ -10,10 +10,10 @@ import {
HoverCard, HoverCard,
Kbd, Kbd,
Modal, Modal,
Stack,
Table, Table,
Text, Text,
Title, Title,
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { import {
IconAnchor, IconAnchor,
@@ -36,6 +36,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 Tip from '../../../layout/Tip'; import Tip from '../../../layout/Tip';
import { usePrimaryGradient } from '../../../layout/useGradient'; import { usePrimaryGradient } from '../../../layout/useGradient';
import Credits from '../../../Settings/Common/Credits'; import Credits from '../../../Settings/Common/Credits';
@@ -198,9 +199,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();
@@ -214,15 +215,19 @@ const useInformationTableItems = (newVersionAvailable?: string): InformationTabl
icon: <IconKey size={20} />, icon: <IconKey size={20} />,
label: 'experimental_disableEditMode', label: 'experimental_disableEditMode',
content: ( content: (
<Stack> <Tooltip
color="red"
withinPortal
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
on the API. This will be removed in future versions, as Homarr will receive a proper
authentication system, which will make this obsolete."
>
<Badge color="red">WARNING</Badge> <Badge color="red">WARNING</Badge>
<Text color="red" size="xs"> </Tooltip>
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
on the API. This will be removed in future versions, as Homarr will receive a proper
authentication system, which will make this obsolete.
</Text>
</Stack>
), ),
}, },
]; ];
@@ -238,7 +243,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>
), ),
@@ -247,7 +252,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>
), ),
@@ -260,7 +265,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>
), ),
@@ -269,7 +274,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>
), ),
@@ -279,7 +284,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 && (
@@ -319,7 +324,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>
), ),

View File

@@ -42,7 +42,7 @@ export function Header(props: any) {
> >
<Search /> <Search />
{!editModeEnabled && <ToggleEditModeAction />} {!editModeEnabled && <ToggleEditModeAction />}
<DockerMenuButton /> {!editModeEnabled && <DockerMenuButton />}
<Indicator <Indicator
size={15} size={15}
color="blue" color="blue"

View File

@@ -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}>

View 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/tryPassword', { tried: values.triedPassword, type: 'edit' })
.then((res) => {
showNotification({
title: 'Success',
message: 'Successfully toggled edit mode, reloading the page...',
color: 'green',
});
setTimeout(() => {
window.location.reload();
}, 500);
})
.catch((_) => {
showNotification({
title: 'Error',
message: 'Failed to toggle edit mode, please try again.',
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> . If it is not set, you are not
able to toggle edit mode on and off.
</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>
);
}

View File

@@ -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;
} }

View File

@@ -2,26 +2,31 @@ import Consola from 'consola';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
function Post(req: NextApiRequest, res: NextApiResponse) { function Post(req: NextApiRequest, res: NextApiResponse) {
const { tried } = req.body; const { tried, type = 'password' } = req.body;
// Try to match the password with the PASSWORD env variable // If the type of password is "edit", we run this branch to check the edit password
if (tried === process.env.PASSWORD) { if (type === 'edit') {
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,
});
}
} else if (tried === process.env.PASSWORD) {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
}); });
} }
// Warn that there was a wrong password attempt (date : wrong password, person's IP)
Consola.warn( Consola.warn(
`${new Date().toLocaleString()} : Wrong password attempt, from ${ `${new Date().toLocaleString()} : Wrong password attempt, from ${
req.headers['x-forwarded-for'] req.headers['x-forwarded-for']
}` }`
); );
return res.status(200).json({ return res.status(401).json({
success: false, success: false,
}); });
} }
export default async (req: NextApiRequest, res: NextApiResponse) => { export default async (req: NextApiRequest, res: NextApiResponse) => {
// Filter out if the request is a POST or a GET
if (req.method === 'POST') { if (req.method === 'POST') {
return Post(req, res); return Post(req, res);
} }

6
vercel.json Normal file
View File

@@ -0,0 +1,6 @@
{
"env": {
"EDIT_MODE_PASSWORD": "edit",
"DISABLE_EDIT_MODE": "TRUE"
}
}