Fixes #4811 -- Fix account recovery and add a recovery URL generator CLI command.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-11-25 17:32:28 -06:00
parent 349c1e580f
commit 4fda3c00d3
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
8 changed files with 179 additions and 58 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import Recover from '~/components/Recover.vue';
export default initBase(Recover);

View File

@ -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',

View File

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

View File

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

View File

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