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
<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>
), ),
}, },
]; ];
@@ -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"
}
}