Move first-time registration to Vue component.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-10-26 19:00:51 -05:00
parent 85fe076161
commit c7d7ae8e87
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
13 changed files with 245 additions and 207 deletions

View File

@ -1,45 +0,0 @@
<?php
return [
'method' => 'post',
'groups' => [
'account' => [
'legend' => __('Account Information'),
'elements' => [
'username' => [
'text',
[
'label' => __('E-mail Address'),
'class' => 'half-width',
'required' => true,
'validators' => ['EmailAddress'],
]
],
'password' => [
'password',
[
'label' => __('Password'),
'required' => true,
]
],
],
],
'submit' => [
'elements' => [
'submit' => [
'submit',
[
'type' => 'submit',
'label' => __('Create Account'),
'class' => 'btn btn-lg btn-primary',
]
],
],
],
],
];

View File

@ -2,7 +2,7 @@
<b-form-group v-bind="$attrs" :label-for="id" :state="fieldState">
<template #default>
<slot name="default" v-bind="{ id, field, state: fieldState }">
<b-form-checkbox :id="id" v-model="field.$model" v-bind="inputAttrs">
<b-form-checkbox :id="id" :name="name" v-model="field.$model" v-bind="inputAttrs">
<slot name="label" :lang="'lang_'+id">
</slot>
@ -44,6 +44,9 @@ export default {
type: String,
required: true
},
name: {
type: String,
},
field: {
type: Object,
required: true

View File

@ -2,10 +2,10 @@
<b-form-group v-bind="$attrs" :label-for="id" :state="fieldState">
<template #default>
<slot name="default" v-bind="{ id, field, state: fieldState }">
<b-form-textarea v-if="inputType === 'textarea'" :id="id" v-model="field.$model"
<b-form-textarea v-if="inputType === 'textarea'" :id="id" :name="name" v-model="field.$model"
:required="isRequired" :number="isNumeric" :trim="inputTrim" v-bind="inputAttrs"
:state="fieldState"></b-form-textarea>
<b-form-input v-else :type="inputType" :id="id" v-model="field.$model"
<b-form-input v-else :type="inputType" :id="id" :name="name" v-model="field.$model"
:required="isRequired" :number="isNumeric" :trim="inputTrim"
v-bind="inputAttrs" :state="fieldState"></b-form-input>
</slot>
@ -48,6 +48,9 @@ export default {
type: String,
required: true
},
name: {
type: String,
},
field: {
type: Object,
required: true

View File

@ -55,20 +55,26 @@ export default {
},
url: () => {
return this.$gettext('This field must be a valid URL.');
},
validatePassword: () => {
return this.$gettext('This password is too common or insecure.');
}
}
}
},
computed: {
errorMessages() {
if (!this.field.$error) {
return [];
}
let errors = [];
_.forEach(this.messages, (message, key) => {
if (!_.has(this.field, key)) {
return;
const isValid = !!_.get(this.field, key, true);
if (!isValid) {
const params = _.get(this.field, ['$params', key], {});
errors.push(message(params));
}
let params = _.get(this.field, '$params.' + key, {});
errors.push(message(params));
});
return errors;

View File

@ -0,0 +1,93 @@
<template>
<div class="public-page">
<div class="card">
<div class="card-body">
<div class="row mb-4">
<div class="col-sm">
<h2 class="card-title mb-0 text-center">
<translate key="lang_hdr_setup">AzuraCast First-Time Setup</translate>
</h2>
<h3 class="text-center">
<small class="text-muted">
<translate key="lang_subhdr_welcome">Welcome to AzuraCast!</translate>
</small>
</h3>
</div>
</div>
<div class="row mb-3">
<div class="col-sm">
<p class="card-text">
<translate key="lang_intro_1">Let's get started by creating your Super Administrator account.</translate>
</p>
<p class="card-text">
<translate key="lang_intro_2">This account will have full access to the system, and you'll automatically be logged in to it for the rest of setup.</translate>
</p>
</div>
</div>
<b-alert variant="danger" :show="error != null">{{ error }}</b-alert>
<form id="login-form" action="" method="post">
<input type="hidden" name="csrf" :value="csrf"/>
<b-wrapped-form-group id="username" name="username" label-class="mb-2" :field="$v.form.username"
input-type="email">
<template #label="{lang}">
<icon icon="email" class="mr-1"></icon>
<translate :key="lang">E-mail Address</translate>
</template>
</b-wrapped-form-group>
<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" block variant="primary" class="mt-2" :disabled="$v.form.$invalid">
<translate key="btn_create_acct">Create Account</translate>
</b-button>
</form>
</div>
</div>
</div>
</template>
<script>
import {validationMixin} from "vuelidate";
import {email, 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: {
username: {required, email},
password: {required, validatePassword}
}
}
},
data() {
return {
form: {
username: null,
password: null,
}
}
}
}
</script>

View File

@ -0,0 +1,7 @@
import {helpers} from 'vuelidate/dist/validators.min.js';
import zxcvbn from "zxcvbn";
export default function validatePassword(value) {
const result = zxcvbn(value);
return !helpers.req(value) || result.score > 2;
}

View File

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

View File

@ -23,6 +23,7 @@ module.exports = {
PublicRequests: '~/pages/Public/Requests.js',
PublicSchedule: '~/pages/Public/Schedule.js',
PublicWebDJ: '~/pages/Public/WebDJ.js',
SetupRegister: '~/pages/Setup/Register.js',
SetupSettings: '~/pages/Setup/Settings.js',
SetupStation: '~/pages/Setup/Station.js',
StationsMedia: '~/pages/Stations/Media.js',

View File

@ -165,9 +165,7 @@ abstract class AbstractApiCrudController
$errors = $this->validator->validate($record);
if (count($errors) > 0) {
$e = new ValidationException((string)$errors);
$e->setDetailedErrors($errors);
throw $e;
throw ValidationException::fromValidationErrors($errors);
}
$this->em->persist($record);

View File

@ -7,6 +7,7 @@ namespace App\Controller\Frontend;
use App\Entity;
use App\Environment;
use App\Exception\NotLoggedInException;
use App\Exception\ValidationException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Session\Flash;
@ -14,6 +15,7 @@ use App\Version;
use App\VueComponent\StationFormComponent;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class SetupController
{
@ -36,113 +38,76 @@ class SetupController
return $response->withRedirect((string)$request->getRouter()->named('setup:' . $current_step));
}
/**
* Determine which step of setup is currently active.
*
* @param ServerRequest $request
*/
protected function getSetupStep(ServerRequest $request): string
{
$settings = $this->settingsRepo->readSettings();
if ($settings->isSetupComplete()) {
return 'complete';
}
// Step 1: Register
$num_users = (int)$this->em->createQuery(
<<<'DQL'
SELECT COUNT(u.id) FROM App\Entity\User u
DQL
)->getSingleScalarResult();
if (0 === $num_users) {
return 'register';
}
// If past "register" step, require login.
$auth = $request->getAuth();
if (!$auth->isLoggedIn()) {
throw new NotLoggedInException();
}
// Step 2: Set up Station
$num_stations = (int)$this->em->createQuery(
<<<'DQL'
SELECT COUNT(s.id) FROM App\Entity\Station s
DQL
)->getSingleScalarResult();
if (0 === $num_stations) {
return 'station';
}
// Step 3: System Settings
return 'settings';
}
/**
* Placeholder function for "setup complete" redirection.
*
* @param ServerRequest $request
* @param Response $response
*/
public function completeAction(ServerRequest $request, Response $response): ResponseInterface
{
$request->getFlash()->addMessage('<b>' . __('Setup has already been completed!') . '</b>', Flash::ERROR);
return $response->withRedirect((string)$request->getRouter()->named('dashboard'));
}
/**
* Setup Step 1:
* Create Super Administrator Account
*/
public function registerAction(ServerRequest $request, Response $response): ResponseInterface
{
public function registerAction(
ServerRequest $request,
Response $response,
Entity\Repository\RolePermissionRepository $permissionRepo,
ValidatorInterface $validator
): ResponseInterface {
// Verify current step.
$current_step = $this->getSetupStep($request);
if ($current_step !== 'register' && $this->environment->isProduction()) {
return $response->withRedirect((string)$request->getRouter()->named('setup:' . $current_step));
}
// Create first account form.
$data = $request->getParams();
$csrf = $request->getCsrf();
if (!empty($data['username']) && !empty($data['password'])) {
// Create actions and roles supporting Super Admninistrator.
$role = new Entity\Role();
$role->setName(__('Super Administrator'));
$error = null;
$this->em->persist($role);
$this->em->flush();
if ($request->isPost()) {
try {
$data = $request->getParams();
$rha = new Entity\RolePermission($role);
$rha->setActionName('administer all');
$csrf->verify($data['csrf'] ?? null, 'register');
$this->em->persist($rha);
if (empty($data['username']) || empty($data['password'])) {
throw new \InvalidArgumentException('Username and password required.');
}
// Create user account.
$user = new Entity\User();
$user->setEmail($data['username']);
$user->setNewPassword($data['password']);
$user->getRoles()->add($role);
$this->em->persist($user);
$role = $permissionRepo->ensureSuperAdministratorRole();
// Write to DB.
$this->em->flush();
// Create user account.
$user = new Entity\User();
$user->setEmail($data['username']);
$user->setNewPassword($data['password']);
$user->getRoles()->add($role);
// Log in the newly created user.
$auth = $request->getAuth();
$auth->authenticate($data['username'], $data['password']);
$errors = $validator->validate($user);
if (count($errors) > 0) {
throw ValidationException::fromValidationErrors($errors);
}
$acl = $request->getAcl();
$acl->reload();
$this->em->persist($user);
$this->em->flush();
return $response->withRedirect((string)$request->getRouter()->named('setup:index'));
// Log in the newly created user.
$auth = $request->getAuth();
$auth->authenticate($data['username'], $data['password']);
$acl = $request->getAcl();
$acl->reload();
return $response->withRedirect((string)$request->getRouter()->named('setup:index'));
} catch (\Throwable $e) {
$error = $e->getMessage();
}
}
return $request->getView()
->renderToResponse($response, 'frontend/setup/register');
return $request->getView()->renderVuePage(
response: $response,
component: 'Vue_SetupRegister',
id: 'setup-register',
layout: 'minimal',
title: __('Set Up AzuraCast'),
props: [
'csrf' => $csrf->generate('register'),
'error' => $error,
]
);
}
/**
@ -206,4 +171,61 @@ class SetupController
],
);
}
/**
* Placeholder function for "setup complete" redirection.
*
* @param ServerRequest $request
* @param Response $response
*/
public function completeAction(ServerRequest $request, Response $response): ResponseInterface
{
$request->getFlash()->addMessage('<b>' . __('Setup has already been completed!') . '</b>', Flash::ERROR);
return $response->withRedirect((string)$request->getRouter()->named('dashboard'));
}
/**
* Determine which step of setup is currently active.
*
* @param ServerRequest $request
*/
protected function getSetupStep(ServerRequest $request): string
{
$settings = $this->settingsRepo->readSettings();
if ($settings->isSetupComplete()) {
return 'complete';
}
// Step 1: Register
$num_users = (int)$this->em->createQuery(
<<<'DQL'
SELECT COUNT(u.id) FROM App\Entity\User u
DQL
)->getSingleScalarResult();
if (0 === $num_users) {
return 'register';
}
// If past "register" step, require login.
$auth = $request->getAuth();
if (!$auth->isLoggedIn()) {
throw new NotLoggedInException();
}
// Step 2: Set up Station
$num_stations = (int)$this->em->createQuery(
<<<'DQL'
SELECT COUNT(s.id) FROM App\Entity\Station s
DQL
)->getSingleScalarResult();
if (0 === $num_stations) {
return 'station';
}
// Step 3: System Settings
return 'settings';
}
}

View File

@ -31,4 +31,11 @@ class ValidationException extends Exception
{
$this->detailedErrors = $detailedErrors;
}
public static function fromValidationErrors(ConstraintViolationListInterface $detailedErrors): self
{
$exception = new self((string)$detailedErrors);
$exception->setDetailedErrors($detailedErrors);
return $exception;
}
}

View File

@ -1,63 +0,0 @@
<?php
$this->layout('minimal', [
'title' => 'Set Up',
'page_class' => 'login-content'
]);
/** @var \App\Assets $assets */
$assets->load('zxcvbn');
?>
<div class="public-page">
<div class="card">
<div class="card-body">
<div class="row mb-4">
<div class="col-sm">
<h2 class="card-title mb-0 text-center">
<?=sprintf(__('AzuraCast First-Time Setup')) ?>
</h2>
<h3 class="text-center">
<small class="text-muted"><?=__('Welcome to AzuraCast!') ?></small>
</h3>
</div>
</div>
<div class="row mb-3">
<div class="col-sm">
<p><?=__('Let\'s get started by creating your Super Administrator account.') ?></p>
<p><?=__('This account will have full access to the system, and you\'ll automatically be logged in to it for the rest of setup.') ?></p>
</div>
</div>
<form id="login-form" action="" method="post">
<div class="row">
<div class="form-group col-sm">
<label for="username" class="mb-2">
<i class="material-icons mr-1" aria-hidden="true">email</i>
<strong><?=__('E-mail Address') ?></strong>
</label>
<input type="email" id="username" name="username" class="form-control" placeholder="" required>
</div>
</div>
<div class="row">
<div class="form-group col-sm">
<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="" required>
</div>
</div>
<div class="row">
<div class="col-sm">
<button type="submit" role="button" title="<?=__('Create Account') ?>" class="btn btn-login btn-primary btn-block mt-2">
<?=__('Create Account') ?>
</button>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -14,9 +14,8 @@ class Frontend_SetupCest extends CestAbstract
$I->amOnPage('/');
$I->see('Setup');
$I->see('Super Administrator');
$I->seeCurrentUrlEquals('/setup/register');
$I->seeInTitle('Set Up');
$I->comment('Setup redirect found.');
}