Move first-time registration to Vue component.
This commit is contained in:
parent
85fe076161
commit
c7d7ae8e87
|
@ -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',
|
||||
]
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
];
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import initBase from '~/base.js';
|
||||
|
||||
import '~/vendor/bootstrapVue.js';
|
||||
|
||||
import SetupRegister from '~/components/Setup/Register.vue';
|
||||
|
||||
export default initBase(SetupRegister);
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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.');
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue