mirror of
https://github.com/nextcloud/server.git
synced 2026-02-03 20:41:22 -05:00
feat(login): Add rememberme checkbox
Only present if allowed by configuration. Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
This commit is contained in:
parent
28b48eec39
commit
4e83d20837
7 changed files with 85 additions and 22 deletions
|
|
@ -143,6 +143,11 @@ class LoginController extends Controller {
|
|||
$this->config->getSystemValue('login_form_autocomplete', true) === true
|
||||
);
|
||||
|
||||
$this->initialState->provideInitialState(
|
||||
'loginCanRememberme',
|
||||
$this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15) > 0
|
||||
);
|
||||
|
||||
if (!empty($redirect_url)) {
|
||||
[$url, ] = explode('?', $redirect_url);
|
||||
if ($url !== $this->urlGenerator->linkToRoute('core.login.logout')) {
|
||||
|
|
@ -287,6 +292,7 @@ class LoginController extends Controller {
|
|||
ITrustedDomainHelper $trustedDomainHelper,
|
||||
string $user = '',
|
||||
string $password = '',
|
||||
bool $rememberme = false,
|
||||
?string $redirect_url = null,
|
||||
string $timezone = '',
|
||||
string $timezone_offset = '',
|
||||
|
|
@ -339,9 +345,10 @@ class LoginController extends Controller {
|
|||
$this->request,
|
||||
$user,
|
||||
$password,
|
||||
$rememberme,
|
||||
$redirect_url,
|
||||
$timezone,
|
||||
$timezone_offset
|
||||
$timezone_offset,
|
||||
);
|
||||
$result = $loginChain->process($data);
|
||||
if (!$result->isSuccess()) {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,17 @@
|
|||
data-login-form-input-password
|
||||
required />
|
||||
|
||||
<NcCheckboxRadioSwitch
|
||||
v-if="remembermeAllowed"
|
||||
id="rememberme"
|
||||
ref="rememberme"
|
||||
name="rememberme"
|
||||
value="1"
|
||||
:checked.sync="rememberme"
|
||||
data-login-form-input-rememberme>
|
||||
{{ t('core', 'Remember me') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<LoginButton data-login-form-submit :loading="loading" />
|
||||
|
||||
<input
|
||||
|
|
@ -117,6 +128,7 @@ import { loadState } from '@nextcloud/initial-state'
|
|||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { generateUrl, imagePath } from '@nextcloud/router'
|
||||
import debounce from 'debounce'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
||||
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
|
|
@ -128,6 +140,7 @@ export default {
|
|||
|
||||
components: {
|
||||
LoginButton,
|
||||
NcCheckboxRadioSwitch,
|
||||
NcPasswordField,
|
||||
NcTextField,
|
||||
NcNoteCard,
|
||||
|
|
@ -166,6 +179,11 @@ export default {
|
|||
default: true,
|
||||
},
|
||||
|
||||
remembermeAllowed: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
directLogin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
|
@ -200,6 +218,7 @@ export default {
|
|||
loading: false,
|
||||
user: props.username,
|
||||
password: '',
|
||||
rememberme: [],
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
:errors="errors"
|
||||
:throttle-delay="throttleDelay"
|
||||
:auto-complete-allowed="autoCompleteAllowed"
|
||||
:rememberme-allowed="remembermeAllowed"
|
||||
:email-states="emailStates"
|
||||
@submit="loading = true" />
|
||||
<NcButton
|
||||
|
|
@ -148,6 +149,7 @@ export default {
|
|||
canResetPassword: loadState('core', 'loginCanResetPassword', false),
|
||||
resetPasswordLink: loadState('core', 'loginResetPasswordLink', ''),
|
||||
autoCompleteAllowed: loadState('core', 'loginAutocomplete', true),
|
||||
remembermeAllowed: loadState('core', 'loginCanRememberme', true),
|
||||
resetPasswordTarget: loadState('core', 'resetPasswordTarget', ''),
|
||||
resetPasswordUser: loadState('core', 'resetPasswordUser', ''),
|
||||
directLogin: query.direct === '1',
|
||||
|
|
|
|||
|
|
@ -26,9 +26,12 @@ class CreateSessionTokenCommand extends ALoginCommand {
|
|||
}
|
||||
|
||||
public function process(LoginData $loginData): LoginResult {
|
||||
$tokenType = IToken::REMEMBER;
|
||||
if ($this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15) === 0) {
|
||||
$loginData->setRememberLogin(false);
|
||||
}
|
||||
if ($loginData->isRememberLogin()) {
|
||||
$tokenType = IToken::REMEMBER;
|
||||
} else {
|
||||
$tokenType = IToken::DO_NOT_REMEMBER;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,11 @@ class LoginData {
|
|||
/** @var IUser|false|null */
|
||||
private $user = null;
|
||||
|
||||
private bool $rememberLogin = true;
|
||||
|
||||
public function __construct(
|
||||
private IRequest $request,
|
||||
private string $username,
|
||||
private ?string $password,
|
||||
private bool $rememberLogin = true,
|
||||
private ?string $redirectUrl = null,
|
||||
private string $timeZone = '',
|
||||
private string $timeZoneOffset = '',
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ use OCP\IUserManager;
|
|||
use OCP\Notification\IManager;
|
||||
use OCP\Security\Bruteforce\IThrottler;
|
||||
use OCP\Security\ITrustedDomainHelper;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
|
|
@ -277,7 +278,7 @@ class LoginControllerTest extends TestCase {
|
|||
'',
|
||||
]
|
||||
];
|
||||
$this->initialState->expects($this->exactly(13))
|
||||
$this->initialState->expects($this->exactly(14))
|
||||
->method('provideInitialState')
|
||||
->willReturnCallback(function () use (&$calls): void {
|
||||
$expected = array_shift($calls);
|
||||
|
|
@ -309,12 +310,16 @@ class LoginControllerTest extends TestCase {
|
|||
'loginAutocomplete',
|
||||
false
|
||||
],
|
||||
[
|
||||
'loginCanRememberme',
|
||||
false
|
||||
],
|
||||
[
|
||||
'loginRedirectUrl',
|
||||
'login/flow'
|
||||
],
|
||||
];
|
||||
$this->initialState->expects($this->exactly(14))
|
||||
$this->initialState->expects($this->exactly(15))
|
||||
->method('provideInitialState')
|
||||
->willReturnCallback(function () use (&$calls): void {
|
||||
$expected = array_shift($calls);
|
||||
|
|
@ -351,7 +356,7 @@ class LoginControllerTest extends TestCase {
|
|||
];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('passwordResetDataProvider')]
|
||||
#[DataProvider('passwordResetDataProvider')]
|
||||
public function testShowLoginFormWithPasswordResetOption($canChangePassword,
|
||||
$expectedResult): void {
|
||||
$this->userSession
|
||||
|
|
@ -386,13 +391,13 @@ class LoginControllerTest extends TestCase {
|
|||
'loginUsername',
|
||||
'LdapUser'
|
||||
],
|
||||
[], [], [],
|
||||
[], [], [], [],
|
||||
[
|
||||
'loginCanResetPassword',
|
||||
$expectedResult
|
||||
],
|
||||
];
|
||||
$this->initialState->expects($this->exactly(13))
|
||||
$this->initialState->expects($this->exactly(14))
|
||||
->method('provideInitialState')
|
||||
->willReturnCallback(function () use (&$calls): void {
|
||||
$expected = array_shift($calls);
|
||||
|
|
@ -445,6 +450,10 @@ class LoginControllerTest extends TestCase {
|
|||
'loginAutocomplete',
|
||||
true
|
||||
],
|
||||
[
|
||||
'loginCanRememberme',
|
||||
false
|
||||
],
|
||||
[],
|
||||
[
|
||||
'loginResetPasswordLink',
|
||||
|
|
@ -455,7 +464,7 @@ class LoginControllerTest extends TestCase {
|
|||
false
|
||||
],
|
||||
];
|
||||
$this->initialState->expects($this->exactly(13))
|
||||
$this->initialState->expects($this->exactly(14))
|
||||
->method('provideInitialState')
|
||||
->willReturnCallback(function () use (&$calls): void {
|
||||
$expected = array_shift($calls);
|
||||
|
|
@ -476,7 +485,19 @@ class LoginControllerTest extends TestCase {
|
|||
$this->assertEquals($expectedResponse, $this->loginController->showLoginForm('0', ''));
|
||||
}
|
||||
|
||||
public function testLoginWithInvalidCredentials(): void {
|
||||
public static function remembermeProvider(): array {
|
||||
return [
|
||||
[
|
||||
true,
|
||||
],
|
||||
[
|
||||
false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('remembermeProvider')]
|
||||
public function testLoginWithInvalidCredentials(bool $rememberme): void {
|
||||
$user = 'MyUserName';
|
||||
$password = 'secret';
|
||||
$loginPageUrl = '/login?redirect_url=/apps/files';
|
||||
|
|
@ -491,6 +512,7 @@ class LoginControllerTest extends TestCase {
|
|||
$this->request,
|
||||
$user,
|
||||
$password,
|
||||
$rememberme,
|
||||
'/apps/files'
|
||||
);
|
||||
$loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD);
|
||||
|
|
@ -509,12 +531,13 @@ class LoginControllerTest extends TestCase {
|
|||
$expected = new RedirectResponse($loginPageUrl);
|
||||
$expected->throttle(['user' => 'MyUserName']);
|
||||
|
||||
$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, '/apps/files');
|
||||
$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme, '/apps/files');
|
||||
|
||||
$this->assertEquals($expected, $response);
|
||||
}
|
||||
|
||||
public function testLoginWithValidCredentials(): void {
|
||||
#[DataProvider('remembermeProvider')]
|
||||
public function testLoginWithValidCredentials(bool $rememberme): void {
|
||||
$user = 'MyUserName';
|
||||
$password = 'secret';
|
||||
$loginChain = $this->createMock(LoginChain::class);
|
||||
|
|
@ -527,7 +550,8 @@ class LoginControllerTest extends TestCase {
|
|||
$loginData = new LoginData(
|
||||
$this->request,
|
||||
$user,
|
||||
$password
|
||||
$password,
|
||||
$rememberme,
|
||||
);
|
||||
$loginResult = LoginResult::success($loginData);
|
||||
$loginChain->expects($this->once())
|
||||
|
|
@ -540,10 +564,11 @@ class LoginControllerTest extends TestCase {
|
|||
->willReturn('/default/foo');
|
||||
|
||||
$expected = new RedirectResponse('/default/foo');
|
||||
$this->assertEquals($expected, $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password));
|
||||
$this->assertEquals($expected, $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme));
|
||||
}
|
||||
|
||||
public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn(): void {
|
||||
#[DataProvider('remembermeProvider')]
|
||||
public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn(bool $rememberme): void {
|
||||
/** @var IUser|MockObject $user */
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->expects($this->any())
|
||||
|
|
@ -567,14 +592,15 @@ class LoginControllerTest extends TestCase {
|
|||
$this->userSession->expects($this->never())
|
||||
->method('createRememberMeToken');
|
||||
|
||||
$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $originalUrl);
|
||||
$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $rememberme, $originalUrl);
|
||||
|
||||
$expected = new RedirectResponse('');
|
||||
$expected->throttle(['user' => 'Jane']);
|
||||
$this->assertEquals($expected, $response);
|
||||
}
|
||||
|
||||
public function testLoginWithoutPassedCsrfCheckAndLoggedIn(): void {
|
||||
#[DataProvider('remembermeProvider')]
|
||||
public function testLoginWithoutPassedCsrfCheckAndLoggedIn(bool $rememberme): void {
|
||||
/** @var IUser|MockObject $user */
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->expects($this->any())
|
||||
|
|
@ -607,13 +633,14 @@ class LoginControllerTest extends TestCase {
|
|||
->with('remember_login_cookie_lifetime')
|
||||
->willReturn(1234);
|
||||
|
||||
$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $originalUrl);
|
||||
$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $rememberme, $originalUrl);
|
||||
|
||||
$expected = new RedirectResponse($redirectUrl);
|
||||
$this->assertEquals($expected, $response);
|
||||
}
|
||||
|
||||
public function testLoginWithValidCredentialsAndRedirectUrl(): void {
|
||||
#[DataProvider('remembermeProvider')]
|
||||
public function testLoginWithValidCredentialsAndRedirectUrl(bool $rememberme): void {
|
||||
$user = 'MyUserName';
|
||||
$password = 'secret';
|
||||
$redirectUrl = 'https://next.cloud/apps/mail';
|
||||
|
|
@ -628,6 +655,7 @@ class LoginControllerTest extends TestCase {
|
|||
$this->request,
|
||||
$user,
|
||||
$password,
|
||||
$rememberme,
|
||||
'/apps/mail'
|
||||
);
|
||||
$loginResult = LoginResult::success($loginData);
|
||||
|
|
@ -644,12 +672,13 @@ class LoginControllerTest extends TestCase {
|
|||
->willReturn($redirectUrl);
|
||||
$expected = new RedirectResponse($redirectUrl);
|
||||
|
||||
$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, '/apps/mail');
|
||||
$response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, $rememberme, '/apps/mail');
|
||||
|
||||
$this->assertEquals($expected, $response);
|
||||
}
|
||||
|
||||
public function testToNotLeakLoginName(): void {
|
||||
#[DataProvider('remembermeProvider')]
|
||||
public function testToNotLeakLoginName(bool $rememberme): void {
|
||||
$loginChain = $this->createMock(LoginChain::class);
|
||||
$trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
|
||||
$trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
|
||||
|
|
@ -662,6 +691,7 @@ class LoginControllerTest extends TestCase {
|
|||
$this->request,
|
||||
'john@doe.com',
|
||||
'just wrong',
|
||||
$rememberme,
|
||||
'/apps/files'
|
||||
);
|
||||
$loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD);
|
||||
|
|
@ -688,6 +718,7 @@ class LoginControllerTest extends TestCase {
|
|||
$trustedDomainHelper,
|
||||
'john@doe.com',
|
||||
'just wrong',
|
||||
$rememberme,
|
||||
'/apps/files'
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ abstract class ALoginTestCommand extends TestCase {
|
|||
$this->request,
|
||||
$this->username,
|
||||
$this->password,
|
||||
true,
|
||||
$this->redirectUrl
|
||||
);
|
||||
$data->setUser($this->user);
|
||||
|
|
@ -94,6 +95,7 @@ abstract class ALoginTestCommand extends TestCase {
|
|||
$this->request,
|
||||
$this->username,
|
||||
$this->password,
|
||||
true,
|
||||
null,
|
||||
$this->timezone,
|
||||
$this->timeZoneOffset
|
||||
|
|
|
|||
Loading…
Reference in a new issue