Fixes #4811 -- Fix account recovery and add a recovery URL generator CLI command.
This commit is contained in:
parent
349c1e580f
commit
4fda3c00d3
|
@ -123,6 +123,11 @@ return function (Application $console) {
|
|||
Command\Users\ListCommand::class
|
||||
)->setDescription('List all accounts in the system.');
|
||||
|
||||
$console->command(
|
||||
'azuracast:account:login-token email',
|
||||
Command\Users\LoginTokenCommand::class
|
||||
)->setDescription('Create a unique login recovery URL for the specified account.');
|
||||
|
||||
$console->command(
|
||||
'azuracast:account:reset-password email',
|
||||
Command\Users\ResetPasswordCommand::class
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div class="public-page">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<div class="mb-3">
|
||||
<h2 class="card-title mb-0 text-center">
|
||||
<translate key="lang_hdr">Recover Account</translate>
|
||||
</h2>
|
||||
<h3 class="text-center">
|
||||
<small class="text-muted">
|
||||
<translate key="lang_subhdr">Choose a new password for your account.</translate>
|
||||
</small>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<b-alert variant="danger" :show="error != null">{{ error }}</b-alert>
|
||||
|
||||
<form id="recover-form" class="form vue-form" action="" method="post">
|
||||
<input type="hidden" name="csrf" :value="csrf"/>
|
||||
|
||||
<b-wrapped-form-group id="password" name="password" label-class="mb-2" :field="$v.form.password"
|
||||
input-type="password">
|
||||
<template #label="{lang}">
|
||||
<icon icon="vpn_key" class="mr-1"></icon>
|
||||
<translate :key="lang">Password</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-button type="submit" size="lg" block variant="primary" :disabled="$v.form.$invalid"
|
||||
class="mt-2">
|
||||
<translate key="btn_submit">Recover Account</translate>
|
||||
</b-button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {validationMixin} from "vuelidate";
|
||||
import {required} from 'vuelidate/dist/validators.min.js';
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
import Icon from "~/components/Common/Icon";
|
||||
import validatePassword from '~/functions/validatePassword.js';
|
||||
|
||||
export default {
|
||||
name: 'SetupRegister',
|
||||
components: {Icon, BWrappedFormGroup},
|
||||
mixins: [
|
||||
validationMixin
|
||||
],
|
||||
props: {
|
||||
csrf: String,
|
||||
error: String,
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
form: {
|
||||
password: {required, validatePassword}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
password: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -47,7 +47,7 @@
|
|||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-button type="submit" size="lg" block :variant="($v.form.$invalid) ? 'danger' : 'primary'"
|
||||
<b-button type="submit" size="lg" block variant="primary" :disabled="$v.form.$invalid"
|
||||
class="mt-2">
|
||||
<translate key="btn_create_acct">Create Account</translate>
|
||||
</b-button>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import initBase from '~/base.js';
|
||||
|
||||
import '~/vendor/bootstrapVue.js';
|
||||
|
||||
import Recover from '~/components/Recover.vue';
|
||||
|
||||
export default initBase(Recover);
|
|
@ -27,6 +27,7 @@ module.exports = {
|
|||
PublicRequests: '~/pages/Public/Requests.js',
|
||||
PublicSchedule: '~/pages/Public/Schedule.js',
|
||||
PublicWebDJ: '~/pages/Public/WebDJ.js',
|
||||
Recover: '~/pages/Recover.js',
|
||||
SetupRegister: '~/pages/Setup/Register.js',
|
||||
SetupSettings: '~/pages/Setup/Settings.js',
|
||||
SetupStation: '~/pages/Setup/Station.js',
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Command\Users;
|
||||
|
||||
use App\Console\Command\CommandAbstract;
|
||||
use App\Entity;
|
||||
use App\Http\RouterInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class LoginTokenCommand extends CommandAbstract
|
||||
{
|
||||
public function __invoke(
|
||||
SymfonyStyle $io,
|
||||
EntityManagerInterface $em,
|
||||
Entity\Repository\UserLoginTokenRepository $loginTokenRepo,
|
||||
RouterInterface $router,
|
||||
string $email
|
||||
): int {
|
||||
$io->title('Generate Account Login Recovery URL');
|
||||
|
||||
$user = $em->getRepository(Entity\User::class)
|
||||
->findOneBy(['email' => $email]);
|
||||
|
||||
if ($user instanceof Entity\User) {
|
||||
$loginToken = $loginTokenRepo->createToken($user);
|
||||
|
||||
$url = $router->named(
|
||||
'account:recover',
|
||||
['token' => $loginToken],
|
||||
[],
|
||||
true
|
||||
);
|
||||
|
||||
$io->text([
|
||||
'The account recovery URL is:',
|
||||
'',
|
||||
' ' . $url,
|
||||
'',
|
||||
'Log in using this temporary URL and set a new password using the web interface.',
|
||||
'',
|
||||
]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$io->error('Account not found.');
|
||||
return 1;
|
||||
}
|
||||
}
|
|
@ -20,9 +20,8 @@ class RecoverAction
|
|||
Entity\Repository\UserLoginTokenRepository $loginTokenRepo,
|
||||
EntityManagerInterface $em
|
||||
): ResponseInterface {
|
||||
$flash = $request->getFlash();
|
||||
|
||||
$user = $loginTokenRepo->authenticate($token);
|
||||
$flash = $request->getFlash();
|
||||
|
||||
if (!$user instanceof Entity\User) {
|
||||
$flash->addMessage(
|
||||
|
@ -36,29 +35,55 @@ class RecoverAction
|
|||
return $response->withRedirect((string)$request->getRouter()->named('account:login'));
|
||||
}
|
||||
|
||||
$csrf = $request->getCsrf();
|
||||
$error = null;
|
||||
|
||||
if ($request->isPost()) {
|
||||
$newPassword = $request->getParsedBodyParam('password');
|
||||
try {
|
||||
$data = $request->getParams();
|
||||
|
||||
$user->setNewPassword($newPassword);
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
$csrf->verify($data['csrf'] ?? null, 'recover');
|
||||
|
||||
$request->getAuth()->setUser($user);
|
||||
if (empty($data['password'])) {
|
||||
throw new \InvalidArgumentException('Password required.');
|
||||
}
|
||||
|
||||
$loginTokenRepo->revokeForUser($user);
|
||||
$user = $request->getUser();
|
||||
$user->setNewPassword($data['password']);
|
||||
$user->setTwoFactorSecret();
|
||||
|
||||
$flash->addMessage(
|
||||
sprintf(
|
||||
'<b>%s</b><br>%s',
|
||||
__('Logged in using account recovery token'),
|
||||
__('Your password has been updated.')
|
||||
),
|
||||
Flash::SUCCESS
|
||||
);
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
return $response->withRedirect((string)$request->getRouter()->named('dashboard'));
|
||||
$request->getAuth()->setUser($user);
|
||||
|
||||
$loginTokenRepo->revokeForUser($user);
|
||||
|
||||
$flash->addMessage(
|
||||
sprintf(
|
||||
'<b>%s</b><br>%s',
|
||||
__('Logged in using account recovery token'),
|
||||
__('Your password has been updated.')
|
||||
),
|
||||
Flash::SUCCESS
|
||||
);
|
||||
|
||||
return $response->withRedirect((string)$request->getRouter()->named('dashboard'));
|
||||
} catch (\Throwable $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'frontend/account/recover');
|
||||
return $request->getView()->renderVuePage(
|
||||
response: $response,
|
||||
component: 'Vue_Recover',
|
||||
id: 'account-recover',
|
||||
layout: 'minimal',
|
||||
title: __('Recover Account'),
|
||||
props: [
|
||||
'csrf' => $csrf->generate('recover'),
|
||||
'error' => $error,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
<?php
|
||||
|
||||
$this->layout(
|
||||
'minimal',
|
||||
[
|
||||
'title' => __('Recover Account'),
|
||||
'page_class' => 'login-content',
|
||||
]
|
||||
);
|
||||
|
||||
/** @var \App\Assets $assets */
|
||||
$assets->load('zxcvbn');
|
||||
?>
|
||||
|
||||
<div class="public-page">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-2 text-center"><?=__('Recover Account')?></h2>
|
||||
<p class="text-center mb-3"><?=__('Choose a new password for your account.')?></p>
|
||||
|
||||
<form id="login-form" action="" method="post">
|
||||
<div class="form-group">
|
||||
<label for="password" class="mb-2">
|
||||
<i class="material-icons mr-1" aria-hidden="true">vpn_key</i>
|
||||
<strong><?=__('Password')?></strong>
|
||||
</label>
|
||||
<input type="password" id="password" name="password" class="strength form-control" placeholder="<?=__(
|
||||
'Enter your password'
|
||||
)?>" aria-label="<?=__('Password')?>" autocomplete="new-password" required autofocus>
|
||||
</div>
|
||||
<button type="submit" role="button" title="<?=__(
|
||||
'Sign in'
|
||||
)?>" class="btn btn-login btn-primary btn-block mt-2">
|
||||
<?=__('Recover Account')?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in New Issue