Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type UserAPI struct {
PasswordStrength int
UserChangeNotifier *UserChangeNotifier
Registration bool
LocalAuthEnabled bool
}

// GetUsers returns all the users
Expand Down Expand Up @@ -396,6 +397,11 @@ func (a *UserAPI) DeleteUserByID(ctx *gin.Context) {
// schema:
// $ref: "#/definitions/Error"
func (a *UserAPI) ChangePassword(ctx *gin.Context) {
if !a.LocalAuthEnabled {
ctx.AbortWithError(403, errors.New("local authentication is disabled"))
return
}

pw := model.UserExternalPass{}
if err := ctx.Bind(&pw); err == nil {
if err := password.ValidateNewPassword(pw.Pass); err != nil {
Expand Down
21 changes: 20 additions & 1 deletion api/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (s *UserSuite) BeforeTest(suiteName, testName string) {
s.notifiedAdd = true
return nil
})
s.a = &UserAPI{DB: s.db, UserChangeNotifier: s.notifier}
s.a = &UserAPI{DB: s.db, UserChangeNotifier: s.notifier, LocalAuthEnabled: true}
}

func (s *UserSuite) AfterTest(suiteName, testName string) {
Expand Down Expand Up @@ -493,6 +493,25 @@ func (s *UserSuite) Test_UpdatePassword_EmptyPassword() {
assert.True(s.T(), password.ComparePassword(user.Pass, []byte("old")))
}

func (s *UserSuite) Test_UpdatePassword_LocalAuthDisabled_Expect403() {
pw, err := password.CreatePassword("old", 5)
require.NoError(s.T(), err)
s.db.CreateUser(&model.User{ID: 1, Name: "jmattheis", Pass: pw})
s.a.LocalAuthEnabled = false

test.WithUser(s.ctx, 1)
s.ctx.Request = httptest.NewRequest("POST", "/user/current/password", strings.NewReader(`{"pass": "new"}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")

s.a.ChangePassword(s.ctx)

assert.Equal(s.T(), 403, s.recorder.Code)
user, err := s.db.GetUserByID(1)
assert.NoError(s.T(), err)
assert.NotNil(s.T(), user)
assert.True(s.T(), password.ComparePassword(user.Pass, []byte("old")))
}

func (s *UserSuite) Test_UpdatePassword_TooLongPassword_Expect400() {
pw, err := password.CreatePassword("old", 5)
require.NoError(s.T(), err)
Expand Down
2 changes: 1 addition & 1 deletion router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
}
sessionHandler := api.SessionAPI{DB: db, NotifyDeleted: streamHandler.NotifyDeletedClient, SecureCookie: conf.Server.SecureCookie, LocalAuthEnabled: conf.LocalAuthEnabled}
userChangeNotifier := new(api.UserChangeNotifier)
userHandler := api.UserAPI{DB: db, PasswordStrength: conf.PassStrength, UserChangeNotifier: userChangeNotifier, Registration: conf.Registration}
userHandler := api.UserAPI{DB: db, PasswordStrength: conf.PassStrength, UserChangeNotifier: userChangeNotifier, Registration: conf.Registration, LocalAuthEnabled: conf.LocalAuthEnabled}

pluginManager, err := plugin.NewManager(db, conf.PluginsDir, g.Group("/plugin/:id/custom/"), streamHandler)
if err != nil {
Expand Down
75 changes: 0 additions & 75 deletions ui/src/common/SettingsDialog.tsx

This file was deleted.

22 changes: 4 additions & 18 deletions ui/src/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ interface IProps {
version: string;
themeMode: ThemeKey;
toggleTheme: VoidFunction;
showSettings: VoidFunction;
logout: VoidFunction;
style: CSSProperties;
setNavOpen: (open: boolean) => void;
Expand All @@ -93,7 +92,6 @@ const Header = ({
logout,
style,
setNavOpen,
showSettings,
themeMode,
}: IProps) => {
const {classes} = useStyles();
Expand Down Expand Up @@ -124,13 +122,7 @@ const Header = ({
</a>
</div>
{loggedIn && (
<Buttons
admin={admin}
name={name}
logout={logout}
setNavOpen={setNavOpen}
showSettings={showSettings}
/>
<Buttons admin={admin} name={name} logout={logout} setNavOpen={setNavOpen} />
)}
<div>
<IconButton
Expand Down Expand Up @@ -158,7 +150,6 @@ const Header = ({
};

const Buttons = ({
showSettings,
name,
admin,
logout,
Expand All @@ -168,7 +159,6 @@ const Buttons = ({
admin: boolean;
logout: VoidFunction;
setNavOpen: (open: boolean) => void;
showSettings: VoidFunction;
}) => {
const {classes} = useStyles();

Expand All @@ -195,13 +185,9 @@ const Buttons = ({
<Link className={classes.link} to="/plugins" id="navigate-plugins">
<ResponsiveButton icon={<Apps />} label="plugins" color="inherit" />
</Link>
<ResponsiveButton
icon={<AccountCircle />}
label={name}
onClick={showSettings}
id="changepw"
color="inherit"
/>
<Link className={classes.link} to="/settings" id="navigate-settings">
<ResponsiveButton icon={<AccountCircle />} label={name} color="inherit" />
</Link>
<ResponsiveButton
icon={<ExitToApp />}
label="Logout"
Expand Down
25 changes: 16 additions & 9 deletions ui/src/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import {HashRouter, Navigate, Route, Routes} from 'react-router';
import Header from './Header';
import Navigation from './Navigation';
import ScrollUpButton from '../common/ScrollUpButton';
import SettingsDialog from '../common/SettingsDialog';
import ElevationForm from '../common/ElevationForm';
import * as config from '../config';
import Applications from '../application/Applications';
import Clients from '../client/Clients';
import Plugins from '../plugin/Plugins';
import Login from '../user/Login';
import Messages from '../message/Messages';
import Settings from '../user/Settings';
import Users from '../user/Users';
import {observer} from 'mobx-react-lite';
import {ConnectionErrorBanner} from '../common/ConnectionErrorBanner';
Expand Down Expand Up @@ -76,17 +76,19 @@ const Layout = observer(() => {
);
const {version} = config.get('version');
const [navOpen, setNavOpen] = React.useState(false);
const [showSettings, setShowSettings] = React.useState(false);

const setTheme = (next: ThemeKey) => {
setCurrentTheme(next);
localStorage.setItem(localStorageThemeKey, next);
};

const toggleTheme = () => {
const nextMap: Record<ThemeKey, ThemeKey> = {
dark: 'light',
light: 'system',
system: 'dark',
};
const next = nextMap[currentTheme];
setCurrentTheme(next);
localStorage.setItem(localStorageThemeKey, next);
setTheme(nextMap[currentTheme]);
};

const authed = (children: React.ReactNode) => (
Expand Down Expand Up @@ -121,7 +123,6 @@ const Layout = observer(() => {
loggedIn={loggedIn}
themeMode={currentTheme}
toggleTheme={toggleTheme}
showSettings={() => setShowSettings(true)}
logout={logout}
setNavOpen={setNavOpen}
/>
Expand All @@ -148,6 +149,15 @@ const Layout = observer(() => {
path="/users"
element={authed(elevated(<Users />))}
/>
<Route
path="/settings"
element={authed(
<Settings
themeMode={currentTheme}
setTheme={setTheme}
/>
)}
/>
<Route path="/plugins" element={authed(<Plugins />)} />
<Route
path="/plugins/:id"
Expand All @@ -162,9 +172,6 @@ const Layout = observer(() => {
</Routes>
</main>
</div>
{showSettings && (
<SettingsDialog fClose={() => setShowSettings(false)} />
)}
<ScrollUpButton />
<SnackbarProvider />
</div>
Expand Down
5 changes: 3 additions & 2 deletions ui/src/tests/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ describe('User', () => {
expect(await count(page, $table.rows())).toBe(3);
});
it('changes password of current user', async () => {
const $changepw = selector.form('#changepw-dialog');
await page.click('#changepw');
const $changepw = selector.form('#changepw-form');
await page.click('#navigate-settings');
await waitForExists(page, selector.heading(), 'Settings');
await page.waitForSelector($changepw.selector());
await page.type($changepw.input('.newpass'), 'changed');
await page.click($changepw.button('.change'));
Expand Down
107 changes: 107 additions & 0 deletions ui/src/user/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import Grid from '@mui/material/Grid';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import Paper from '@mui/material/Paper';
import Select from '@mui/material/Select';
import TextField from '@mui/material/TextField';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import {observer} from 'mobx-react-lite';
import DefaultPage from '../common/DefaultPage';
import ElevationForm from '../common/ElevationForm';
import {ThemeKey} from '../layout/theme';
import {useStores} from '../stores';
import * as config from '../config';

interface IProps {
themeMode: ThemeKey;
setTheme: (theme: ThemeKey) => void;
}

const Settings = observer(({themeMode, setTheme}: IProps) => {
const [pass, setPass] = useState('');
const {currentUser, elevateStore} = useStores();
const localAuthEnabled = config.get('localAuth');

const submit = () => {
currentUser.changePassword(pass);
setPass('');
};

return (
<DefaultPage title="Settings" maxWidth={400}>
<Grid size={{xs: 12}}>
<Paper elevation={6} sx={{padding: 2}}>
<Typography variant="h6" sx={{marginBottom: 2}}>
Appearance
</Typography>
<FormControl fullWidth>
<InputLabel id="theme-select-label">Theme</InputLabel>
<Select
labelId="theme-select-label"
className="theme-select"
label="Theme"
value={themeMode}
onChange={(e) => setTheme(e.target.value)}>
<MenuItem value="light">Light</MenuItem>
<MenuItem value="dark">Dark</MenuItem>
<MenuItem value="system">System</MenuItem>
</Select>
</FormControl>
</Paper>
</Grid>
<Grid size={{xs: 12}}>
<Paper elevation={6} sx={{padding: 2}} id="changepw-form">
<Typography variant="h6" sx={{marginBottom: 2}}>
Change Password
</Typography>
{localAuthEnabled && !elevateStore.elevated ? (
<ElevationForm />
) : (
<form
onSubmit={(e) => {
e.preventDefault();
submit();
}}>
<TextField
className="newpass"
margin="dense"
type="password"
label="New Password *"
value={pass}
disabled={!localAuthEnabled}
onChange={(e) => setPass(e.target.value)}
fullWidth
/>
<Tooltip
title={
!localAuthEnabled
? 'Password login is disabled on this server.'
: pass.length !== 0
? ''
: 'Password is required'
}>
<div>
<Button
className="change"
type="submit"
disabled={!localAuthEnabled || pass.length === 0}
color="primary"
variant="contained"
fullWidth>
Change
</Button>
</div>
</Tooltip>
</form>
)}
</Paper>
</Grid>
</DefaultPage>
);
});

export default Settings;
Loading