Skip to content
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

Merged
merged 14 commits into from
Sep 21, 2018
Merged

Make login form #266

merged 14 commits into from
Sep 21, 2018

Conversation

nikophil
Copy link
Contributor

@nikophil nikophil commented Sep 11, 2018

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.

@nikophil nikophil changed the title Make login form (WIP) Make login form Sep 12, 2018
Copy link
Member

@weaverryan weaverryan left a 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!


$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents('config/packages/security.yaml'));
Copy link
Member

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?

$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');
Copy link
Member

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 in security.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.

Copy link
Contributor Author

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

'Security\\'
)->getFullName()
);
}
Copy link
Member

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 :).

Copy link
Contributor Author

@nikophil nikophil Sep 14, 2018

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)

);

if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) {
$command->addArgument('controller-class', InputArgument::OPTIONAL);
Copy link
Member

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 :).

'user_fully_qualified_class_name' => $input->getArgument('user-class'),
'user_class_name' => substr(strrchr($input->getArgument('user-class'), '\\'), 1),
]
: []
Copy link
Member

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');
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok that's fair ;)

@@ -184,4 +185,26 @@ public static function entityExists(string $className = null, array $entities =

return $className;
}

public static function classDoesNotExist($className)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returns : string

return $className;
}

public static function classIsUserInterface($userClassName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returns : string

)
->addExtraDependencies('doctrine')
->addExtraDependencies('security')
->addExtraDependencies('twig')
Copy link
Member

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.

->assert(
function (string $output) {
$this->assertContains('Success', $output);
}
Copy link
Member

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.

);

if (Kernel::VERSION_ID < 40100) {
$makerTestAuthenticatorLoginFormUserEntity->addExtraDependencies('symfony/form');
Copy link
Member

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.

Copy link
Member

@weaverryan weaverryan left a 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.

$dependencies = new DependencyBuilder();
$dependencies->addClassDependency(
TwigBundle::class,
'tiwg'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

twig

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oups, sorry for that

'authenticator/EmptySecurityController.tpl.php',
[
'parent_class_name' => \method_exists(AbstractController::class, 'getParameter') ? 'AbstractController' : 'Controller',
]
Copy link
Member

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">
Copy link
Member

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"
Copy link
Member

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?

}

$this->pendingOperations = [];
}

private function generateFileContents(string $targetPath, array $templateData)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

: string

@@ -84,6 +85,18 @@ public function dumpFile(string $targetPath, string $contents)
];
}

public function getFileContents(string $targetPath): string
Copy link
Member

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()

Copy link
Contributor Author

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 ;)

->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!');
Copy link
Member

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.

TwigBundle::class,
'twig'
);
$missingPackagesMessage = 'Twig must be installed to display login form';
Copy link
Member

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.

$missingPackagesMessage = $dependencies->getMissingPackagesMessage(self::getCommandName(), $missingPackagesMessage);
if ($missingPackagesMessage) {
throw new RuntimeCommandException($missingPackagesMessage);
}
Copy link
Member

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.

Form::class,
'symfony/form'
);
$missingPackagesMessage = 'Twig and symfony/form must be installed to display login form';
Copy link
Member

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.

$input->getArgument('authenticator-class'),
'authenticator/EmptyAuthenticator.tpl.php',
[]
);
Copy link
Member

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.

);
$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)
Copy link
Member

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.

}
}

private function generateFormLoginFiles(InputInterface $input)
Copy link
Member

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 :)

Copy link
Member

@weaverryan weaverryan left a 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().

// authenticator type
$authenticatorTypeValues = [
'Empty authenticator' => self::AUTH_TYPE_EMPTY_AUTHENTICATOR,
'Form login' => self::AUTH_TYPE_FORM_LOGIN,
Copy link
Member

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__);
Copy link
Member

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'));

Copy link
Member

@weaverryan weaverryan left a 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!

}

if (self::AUTH_TYPE_FORM_LOGIN === $authenticatorType) {
$nextTexts[] = sprintf('- You must provide a valid redirection in the method <info>%s::onAuthenticationSuccess()</info>.', $authenticatorClass);
Copy link
Member

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'));
Copy link
Member

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__);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... a valid redirect inside ...


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>.';
Copy link
Member

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.

Copy link
Member

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

$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);
Copy link
Member

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.

}

if (!$this->userNeedsEncoder($securityData, $userClass)) {
$nextTexts[] = sprintf('- Check user\'s password in <info>%s::checkCredentials()</info>.', $authenticatorClass);
Copy link
Member

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;
}
}
Copy link
Member

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
Copy link
Member

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?

Copy link
Contributor Author

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 👍

}
$io->text($text);

return $nextTexts;
Copy link
Member

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>
Copy link
Member

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

Copy link
Contributor Author

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

@weaverryan
Copy link
Member

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.

@weaverryan weaverryan merged commit 5257b67 into symfony:master Sep 21, 2018
weaverryan added a commit that referenced this pull request Sep 21, 2018
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants