-
-
Notifications
You must be signed in to change notification settings - Fork 416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make login form #266
Make login form #266
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Phew! Big work! I'm very excited about this! Though, I'll admit, it's even more complex than I was thinking - probably the most complex maker yet. Some comments to get things rolling!
src/Maker/MakeAuthenticator.php
Outdated
|
||
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path)); | ||
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents('config/packages/security.yaml')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if this doesn't exist? Should be a huge edge-case - the dependencies should already be checked by now. Maybe we just throw an exception?
src/Maker/MakeAuthenticator.php
Outdated
$command->addOption('entry-point', null, InputOption::VALUE_OPTIONAL); | ||
if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type') | ||
&& !isset($securityData['security']['providers']) || !$securityData['security']['providers']) { | ||
throw new RuntimeCommandException('You need to have at least one provider defined in security.yaml'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To generate a form login authentication, you must configure at least one entry under
providers
insecurity.yaml
.
I'm just making it a bit more clear. There is actually a case (stateless API) where you do NOT need to have a user provider (having it doesn't hurt, but it literally would never be called). So, I want to be clear that WE think you need one for this type of authenticator (because a form login would never be "stateless").
And, could we move this down to right before we call InteractiveSecurityHelper::guessUserClass()
? Up here, it feels a bit unrelated to anything. Also, let's make InteractiveSecurityHelper::guessUserClass()
accept only the providers config, instead of all of the config. It will all just read a bit better: we (1) do the above check to make sure it exists, and throw an exception if it doesn't/is empty. THEN we pass just this for-sure-not-empty providers array into InteractiveSecurityHelper::guessUserClass
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pretty similar than the next comment you've made : once the user has chosen "form login", we already know that we should raise an exception, but, if i put it down before InteractiveSecurityHelper::guessUserClass()
, we're asking 4 questions before raising the exception... i think this could be annoying for the user.
What's your feeling about that ? - but i'm ok that in the code it seems unrelated to anything... at the beginning this test was right before guessUserClass()
call...
ok for other comments
src/Maker/MakeAuthenticator.php
Outdated
'Security\\' | ||
)->getFullName() | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooof. This is ugly for a check for a common thing :). Ok. Let's keep it. Usually, we just let the generator throw an error later. So, I'm not sure I want to make a habit of doing this. However, because this authenticator asks several other questions after this, it would be super annoying to answer them all, and THEN get a fatal error. So, keep it :).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe i could just not use the question's validator ? but we're losing the behavior when the console asks again the answer if the user has provided an existing class...
(even if i know this edge case will be very uncommon)
src/Maker/MakeAuthenticator.php
Outdated
); | ||
|
||
if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) { | ||
$command->addArgument('controller-class', InputArgument::OPTIONAL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional? I don't think it matters, because we're always passing it. But you use required above - let's be consistent. This looks required :).
src/Maker/MakeAuthenticator.php
Outdated
'user_fully_qualified_class_name' => $input->getArgument('user-class'), | ||
'user_class_name' => substr(strrchr($input->getArgument('user-class'), '\\'), 1), | ||
] | ||
: [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found the ternary you were asking about :p. I think an if statement around this entire block is much more clear. Actually, even a switch-case
switch ($input->getArgument('authenticator-type')) {
case self::AUTH_TYPE_FORM_LOGIN:
$generator->generateClass(
// ...
);
break;
case self::self::AUTH_TYPE_EMPTY_AUTHENTICATOR:
$generator->generateClass(
// ...
);
break;
default:
throw new \Exception('Invalid authenticator type');
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok that's fair ;)
src/Validator.php
Outdated
@@ -184,4 +185,26 @@ public static function entityExists(string $className = null, array $entities = | |||
|
|||
return $className; | |||
} | |||
|
|||
public static function classDoesNotExist($className) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
returns : string
src/Validator.php
Outdated
return $className; | ||
} | ||
|
||
public static function classIsUserInterface($userClassName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
returns : string
tests/Maker/FunctionalTest.php
Outdated
) | ||
->addExtraDependencies('doctrine') | ||
->addExtraDependencies('security') | ||
->addExtraDependencies('twig') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This extra dependency thing should be rare. doctrine
make sense to me - that's not a requirement of the Maker, but we're testing that case. But, shouldn't security be a dependency? I suppose twig
is correct to stay here too, because it's only a conditional dependency - i.e. if the choose the "login form" version. Our test system doesn't support those "optional" dependencies. But, we should do a manual check in the maker to make sure they have this if the choose the login-form option. I think I do this in MakeUser - I do a manual check for Doctrine, if they choose the entity option.
tests/Maker/FunctionalTest.php
Outdated
->assert( | ||
function (string $output) { | ||
$this->assertContains('Success', $output); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is necessary. I think we either already check for this always, or, we are at least checking that the process exited successfully, which is basically the same as this.
tests/Maker/FunctionalTest.php
Outdated
); | ||
|
||
if (Kernel::VERSION_ID < 40100) { | ||
$makerTestAuthenticatorLoginFormUserEntity->addExtraDependencies('symfony/form'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could instead add this into the Maker. Then, the user would also see this dependency error. And, I think the tests would also automatically add it, as it WOULD be a dependency when the user is running 4.0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a full review - I was just looking at your replies to my comments and the code associated with them.
src/Maker/MakeAuthenticator.php
Outdated
$dependencies = new DependencyBuilder(); | ||
$dependencies->addClassDependency( | ||
TwigBundle::class, | ||
'tiwg' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
twig
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oups, sorry for that
src/Maker/MakeAuthenticator.php
Outdated
'authenticator/EmptySecurityController.tpl.php', | ||
[ | ||
'parent_class_name' => \method_exists(AbstractController::class, 'getParameter') ? 'AbstractController' : 'Controller', | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About Generator::generateController()
, that's a good idea. We would need to allow a custom template name to be passed as an argument. But at least the new Generator::generateController()
method could handle this logic, and always pass that parent_class_name
. Good idea.
</div> | ||
#} | ||
|
||
<button class="btn btn-lg btn-primary btn-block" type="submit"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove btn-block - that only looks right if you have that full login markup/css file I linked to above
composer.json
Outdated
"symfony/framework-bundle": "^3.4|^4.0", | ||
"symfony/http-kernel": "^3.4|^4.0" | ||
"symfony/http-kernel": "^3.4|^4.0", | ||
"symfony/security": "^3.4|^4.1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
symfony/security
and symfony/form
shouldn't be dependencies. What did you need them for?
src/Generator.php
Outdated
} | ||
|
||
$this->pendingOperations = []; | ||
} | ||
|
||
private function generateFileContents(string $targetPath, array $templateData) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
: string
src/Generator.php
Outdated
@@ -84,6 +85,18 @@ public function dumpFile(string $targetPath, string $contents) | |||
]; | |||
} | |||
|
|||
public function getFileContents(string $targetPath): string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, for this, I meant that we should only have a getFileContentsForPendingOperation()
- and it would contain the code here + the code that is currently in getFileContentsForPendingOperation()
. No need for one called getFileContents()
- just one public function called getFileContentsForPendingOperation()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ype, i don't know why i've done this ;)
src/Maker/MakeAuthenticator.php
Outdated
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeAuth.txt')); | ||
} | ||
|
||
public function interact(InputInterface $input, ConsoleStyle $io, Command $command) | ||
{ | ||
if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) { | ||
return; | ||
throw new RuntimeCommandException('File "security.yaml" does not exist!'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file "config/packages/security.yaml" does not exist. This command requires that file to exist so that it can be updated.
src/Maker/MakeAuthenticator.php
Outdated
TwigBundle::class, | ||
'twig' | ||
); | ||
$missingPackagesMessage = 'Twig must be installed to display login form'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... to display the login form.
src/Maker/MakeAuthenticator.php
Outdated
$missingPackagesMessage = $dependencies->getMissingPackagesMessage(self::getCommandName(), $missingPackagesMessage); | ||
if ($missingPackagesMessage) { | ||
throw new RuntimeCommandException($missingPackagesMessage); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could at least put all of this dependency-builder logic into some protected function in the base Maker so that it's easier to do these manual checks.
src/Maker/MakeAuthenticator.php
Outdated
Form::class, | ||
'symfony/form' | ||
); | ||
$missingPackagesMessage = 'Twig and symfony/form must be installed to display login form'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... to display the login form.
src/Maker/MakeAuthenticator.php
Outdated
$input->getArgument('authenticator-class'), | ||
'authenticator/EmptyAuthenticator.tpl.php', | ||
[] | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put a return
here and then the rest of the code doesn't need to live in an else
.
src/Maker/MakeAuthenticator.php
Outdated
); | ||
$text[] = "Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample; | ||
} | ||
$io->text($text); | ||
} | ||
|
||
public function configureDependencies(DependencyBuilder $dependencies) | ||
private function generateAuthenticatorClass(InputInterface $input) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a nit-pick, but I think we should pass the specific arguments (I think it's 5 in total) that we need here, instead of the $input
. It just makes this method a bit simpler.
src/Maker/MakeAuthenticator.php
Outdated
} | ||
} | ||
|
||
private function generateFormLoginFiles(InputInterface $input) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here - just pass in the args you need, instead of $input
:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just started playing with the command - super fun :). But, I'm out of time at this exact moment :/. One thing that I did notice is the "Next:" message. I think this might need to be a bit more customized for a few cases. The current one probably works for the "empty" authenticator. But if you are making a login form... isn't everything done for you? Except for the onAuthenticationSuccess()
redirect? Is there any variance between the other situations? I mean, is this always the only step that a user needs to do, or is there sometimes other stuff? Probably if your user is not an entity... you should at least review getUser()
.
src/Maker/MakeAuthenticator.php
Outdated
// authenticator type | ||
$authenticatorTypeValues = [ | ||
'Empty authenticator' => self::AUTH_TYPE_EMPTY_AUTHENTICATOR, | ||
'Form login' => self::AUTH_TYPE_FORM_LOGIN, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Login form authenticator
return new RedirectResponse($targetPath); | ||
} | ||
|
||
throw new \Exception('TODO: provide a valid redirection inside '.__FILE__); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could put some example code here in a comment above:
// return new RedirectResponse($this->router->generate('some_route'));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the last round of changes :). I played with the command quite a bit - it's a delight. Even if I don't pay attention and run the most complex case (make:user
with a model class + make:auth
for a login form), each time I refresh, an exception tells me where to go. It's awesome!
src/Maker/MakeAuthenticator.php
Outdated
} | ||
|
||
if (self::AUTH_TYPE_FORM_LOGIN === $authenticatorType) { | ||
$nextTexts[] = sprintf('- You must provide a valid redirection in the method <info>%s::onAuthenticationSuccess()</info>.', $authenticatorClass); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finish the redirect "TODO" in the %s::onAuthenticationSuccess() method.
return new RedirectResponse($targetPath); | ||
} | ||
|
||
// e.g. : return new RedirectResponse($this->router->generate('some_route')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// For example: return
} | ||
|
||
// e.g. : return new RedirectResponse($this->router->generate('some_route')); | ||
throw new \Exception('TODO: provide a valid redirection inside '.__FILE__); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... a valid redirect inside ...
src/Maker/MakeAuthenticator.php
Outdated
|
||
if (self::AUTH_TYPE_FORM_LOGIN === $authenticatorType) { | ||
$nextTexts[] = sprintf('- You must provide a valid redirection in the method <info>%s::onAuthenticationSuccess()</info>.', $authenticatorClass); | ||
$nextTexts[] = '- Review & adapt the login template : <info>/templates/security/login.html.twig</info>.'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Review & adapt the login template: templates/security/login.html.twig.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And let's move this down below the next two. That way, if there are multiple things you need to do in the authenticator, we should all of them, and THEN the template text
src/Maker/MakeAuthenticator.php
Outdated
$nextTexts[] = '- Review & adapt the login template : <info>/templates/security/login.html.twig</info>.'; | ||
|
||
if (!$this->doctrineHelper->isClassAMappedEntity($userClass)) { | ||
$nextTexts[] = sprintf('- Review <info>%s::getUser()</info>, if it match your needs.', $authenticatorClass); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Review %s::getUser() to make sure it matches your needs.
src/Maker/MakeAuthenticator.php
Outdated
} | ||
|
||
if (!$this->userNeedsEncoder($securityData, $userClass)) { | ||
$nextTexts[] = sprintf('- Check user\'s password in <info>%s::checkCredentials()</info>.', $authenticatorClass); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Check the user's password in %s::checkCredentials().
{ | ||
return User::class === $class; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit pick - but could we "slim down" some of these fixture classes, so that they only have what we need?
public function checkCredentials($credentials, UserInterface $user) | ||
{ | ||
<?= $user_needs_encoder ? "return \$this->passwordEncoder->isPasswordValid(\$user, \$credentials['password']);\n" | ||
: "// Check the user’s password or other credentials and return true or false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the '
in user's
is the wrong type of apostrophe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wow you got god eyes 👍
src/Maker/MakeAuthenticator.php
Outdated
} | ||
$io->text($text); | ||
|
||
return $nextTexts; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about one more on the end?
When you're ready, see your login form by going to
login
.
|
||
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> | ||
<label for="inputEmail" class="sr-only">Email address</label> | ||
<input type="email" value="{{ last_username }}" name="<?= $username_field; ?>" id="inputEmail" class="form-control" placeholder="Email address" required autofocus> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah.... this should only be type="email"
if their "username" field contains the term email
I think. I chose username
for my User class, and so this field should not have been type="email".
Also, though annoying, we should probably make the id=""
and label's for=""
a bit dynamic - based on the actual identity field name.
AND... of course - the label's text - Email address
- should be dynamic - probably we can turn their propery name into title-case words - e.g. emailAddress
-> Email Address
or email
to just Email
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok you're absolutely right
Wow! Thank you Nicolas! This is really, really cool - and was a lot of work :). As we discussed, there is one new test failing - we'll address it in another PR before we tag. |
This PR was squashed before being merged into the 1.0-dev branch (closes #266). Discussion ---------- Make login form Improvements for the command `make:auth` in order to create a form login. Now, the `make:auth` command asks if the user wants an empty authenticator or a form login. If "form login" is chosen, the user is asked for the security controller name, and for the user class, if needed. If the controller already exists, the command adds the login / logout methods in the existing controller. Commits ------- 5257b67 form login : PR review : batch 5 cd86b3b form login : PR review : batch 4 78abbf4 form login : fix after review - batch 3 bc0089d form login : use dynamic username field 388df31 form login : guess if encoder is needed c44f878 fix after review - batch 1 9ec9766 Test logging process with users as entity fd76519 unit tests 7fd5a35 Add tests into fixtures files 02ff624 Functional tests f96c51e Add logout method to already existing SecurityController 0bfd028 Handle when securityController provided already exist 011b1d6 Merge MakeLoginForm into MakeAuthenticator efb19e4 create MakeFormLogin command
Improvements for the command
make:auth
in order to create a form login.Now, the
make:auth
command asks if the user wants an empty authenticator or a form login.If "form login" is chosen, the user is asked for the security controller name, and for the user class, if needed.
If the controller already exists, the command adds the login / logout methods in the existing controller.