HA
Recuperação de senha através de link seguro!
Pessoal boa noite,
Estou utilizando o framework a quase um mês, e resolvi implantar uma lógica de recuperação de senha, utilizando o mesmo template da tela de login. A ideia é o seguinte: O usuário clica num link adicionado na tela de login de "Esqueceu a senha", é levado para uma tela onde ele digita o email cadastrado e o sistema envia um email com o link para reset, ao clicar nesse link, faço a checagem se é válido e o usuário digita a nova senha.
1º - Adicionar colunas na tabelas de usuários
2º - Adicionar os campos acima no SystemUser para permitir edição
3º - Alterei na SystemUserForm para não permitir o cadastro de email já existente, hoje o template valida apenas login
4º - Controller SendEmailPasswordForm responsável por capturar o email do usuário e enviar o link para reset
5º - Controller ResetPasswordForm responsável por checar se o link é válido e permitir o usuário digitar a nova senha
Estou utilizando o framework a quase um mês, e resolvi implantar uma lógica de recuperação de senha, utilizando o mesmo template da tela de login. A ideia é o seguinte: O usuário clica num link adicionado na tela de login de "Esqueceu a senha", é levado para uma tela onde ele digita o email cadastrado e o sistema envia um email com o link para reset, ao clicar nesse link, faço a checagem se é válido e o usuário digita a nova senha.
1º - Adicionar colunas na tabelas de usuários
ALTER TABLE `system_user` ADD `reset_pass` CHAR(1) NULL DEFAULT NULL AFTER `active`,
ADD `data_pass` VARCHAR(50) NULL DEFAULT NULL AFTER `reset_pass`,
ADD `uid_pass` VARCHAR(50) NULL DEFAULT NULL AFTER `data_pass`;
2º - Adicionar os campos acima no SystemUser para permitir edição
- <?php
- public function __construct($id = NULL)
- {
- //....
- parent::addAttribute('reset_pass');
- parent::addAttribute('data_pass');
- parent::addAttribute('uid_pass');
- }
- ?>
3º - Alterei na SystemUserForm para não permitir o cadastro de email já existente, hoje o template valida apenas login
- <?php
- public static function onSave($param)
- {
- //....
- if (SystemUser::newFromLogin($object->login) instanceof SystemUser)
- {
- throw new Exception(_t('An user with this login is already registered'));
- }
- if (SystemUser::newFromEmail($object->email) instanceof SystemUser)
- {
- throw new Exception(_t('An user with this email is already registered'));
- }
- //...
- }
- ?>
4º - Controller SendEmailPasswordForm responsável por capturar o email do usuário e enviar o link para reset
- <?php
- class SendEmailPasswordForm extends TPage
- {
- protected $form; // form
- function __construct($param)
- {
- parent::__construct();
- $table = new TTable;
- $table->width = '100%';
- // creates the form
- $this->form = new TForm('form_send_email_pass');
- $this->form->class = 'tform';
- $this->form->style = 'max-width: 450px; margin:auto; margin-top:120px;';
- // add the notebook inside the form
- $this->form->add($table);
- // create the form fields
- $email = new TEntry('email');
- // define the sizes
- $email->setSize('70%', 40);
- $email->style = 'height:35px; font-size:14px;float:left;border-bottom-left-radius: 0;border-top-left-radius: 0;';
- $row=$table->addRow();
- $row->addCell( new TLabel('Resetar senha') )->colspan = 2;
- $row->class='tformtitle';
- $email->placeholder = 'user@email.com';
- $email->setLabel('Email');
- $email->addValidation('Email', new TEmailValidator);
- $envelope = '<span style="float:left;width:35px;margin-left:45px;height:35px;" class="input-group-addon"><span class="glyphicon glyphicon-envelope"></span></span>';
- $container1 = new TElement('div');
- $container1->add($envelope);
- $container1->add($email);
- $row=$table->addRow();
- $row->addCell($container1)->colspan = 2;
- // create an action button (save)
- $recuperar_button=new TButton('recovery');
- // define the button action
- $recuperar_button->setAction(new TAction(array($this, 'onReset')), _t('Send'));
- $recuperar_button->class = 'btn btn-success';
- $recuperar_button->style = 'font-size:18px;width:90%;padding:10px';
- $row=$table->addRow();
- $row->class = 'tformaction';
- $cell = $row->addCell( $recuperar_button );
- $cell->colspan = 2;
- $cell->style = 'text-align:center';
- $login = new TActionLink('Login', new TAction(array($this, 'onLogin')) );
- $login->style = 'font-size:16px;width:90%;padding:10px';
- $row = $table->addRow();
- $cell = $row->addCell( $login );
- $this->form->setFields(array($email, $recuperar_button));
- // add the form to the page
- parent::add($this->form);
- }
- public function onLogin()
- {
- AdiantiCoreApplication::gotoPage('LoginForm');
- }
- public function onReset()
- {
- try
- {
- TTransaction::open('permission');
- $data = $this->form->getData('StdClass');
- $this->form->validate();
- $user = SystemUser::newFromEmail( $data->email );
- TTransaction::close();
- // valida se existe usuário com email digitado
- if ($user)
- {
- try
- {
- TTransaction::open('permission');
- $object = new SystemUser($user->id);
- $object->reset_pass = 'Y'; //parâmetro que diz que há uma requisição de reset de senha
- $object->data_pass = time(); //variável para validação de link
- $object->uid_pass = uniqid(rand(), true); //variável para validação de link
- $object->store();
- // monta parâmetros GET com criptografia
- $url = sprintf( 'id=%s&email=%s&uid=%s&key=%s',$object->id, md5($object->email), md5($object->uid_pass), md5($object->data_pass) );
- // monta link que será enviado para o email do usuário
- $link = ($_SERVER['HTTPS']=='on' ? "https" : "http") . '://'. $_SERVER['HTTP_HOST'] . '/index.php?class=ResetPasswordForm&'. $url;
- // pega parâmetros para envio de email
- $prefs = SystemPreference::getAllPreferences();
- TTransaction::close();
- $mail = new TMail;
- $mail->setFrom($prefs['mail_from'], 'Falko ERP');
- $mail->setSubject('Reset de senha');
- $mail->setHtmlBody("Você solicitou o reset de senha, por favor clique no link abaixo para cadastrar sua nova senha!<br><br>
- Usuário: $object->login<br>
- <a href='$link'>Resetar senha</a><br><br>
- Atenciosamente,<br>
- <b>Falko ERP</b>");
- $mail->addAddress($object->email);
- $mail->SetUseSmtp();
- $mail->SetSmtpHost($prefs['smtp_host'], $prefs['smtp_port']);
- $mail->SetSmtpUser($prefs['smtp_user'], $prefs['smtp_pass']);
- $mail->send();
- new TMessage('info', 'Você receberá um email com as instruções para redefinição de senha.');
- }
- catch (Exception $e)
- {
- // shows the exception error message
- new TMessage('error', $e->getMessage());
- // undo all pending operations
- TTransaction::rollback();
- }
- }
- else
- {
- throw new Exception(_t('User not found'));
- }
- }
- catch (Exception $e)
- {
- new TMessage('error',$e->getMessage());
- TTransaction::rollback();
- }
- }
- }
- ?>
5º - Controller ResetPasswordForm responsável por checar se o link é válido e permitir o usuário digitar a nova senha
- <?php
- class ResetPasswordForm extends TPage
- {
- protected $form; // form
- /**
- * Class constructor
- * Creates the page and the registration form
- */
- function __construct($param)
- {
- parent::__construct();
- $valido = TRUE;
- if (isset($_GET['id']))
- {
- $id = $_GET['id'];
- $email = $_GET['email'];
- $uid = $_GET['uid'];
- $data = $_GET['key'];
- // checa se id do usuário existe
- TTransaction::open('permission');
- $user = new SystemUser($id);
- TTransaction::close();
- if ($user instanceof SystemUser)
- {
- if ($user->active == 'N') //não permite que usuário inativo altera a senha
- {
- $valido = FALSE;
- }
- else if ($user->reset_pass != 'Y') //valida se há uma requisição de reset de senha
- {
- $valido = FALSE;
- }
- else if (md5($user->email) != $email) //valida se email criptografado é igual ao do link acessado
- {
- $valido = FALSE;
- }
- else if (md5($user->uid_pass) != $uid) //valida se uid criptografado é igual ao do link acessado
- {
- $valido = FALSE;
- }
- else if (md5($user->data_pass) != $data) //valida se email criptografado é igual ao do link acessado
- {
- $valido = FALSE;
- }
- }
- else
- {
- $valido = FALSE;
- }
- }
- // se não é valido, leva usuário para tela de login
- if (!$valido && isset($_GET['id']))
- {
- new TMessage('error', _t('User not found'), new TAction(array($this, 'onLogin')) );
- return false;
- }
- $table = new TTable;
- $table->width = '100%';
- // creates the form
- $this->form = new TForm('form_reset_password');
- $this->form->class = 'tform';
- $this->form->style = 'max-width: 450px; margin:auto; margin-top:120px;';
- // add the notebook inside the form
- $this->form->add($table);
- // create the form fields
- $user_id = new THidden('user_id');
- $user_id->setValue($user->id);
- $senha = new TPassword('senha');
- $senha_confirm = new TPassword('senha_confirm');
- $senha->placeholder = 'Nova senha';
- $senha_confirm->placeholder = 'Digite novamente';
- // define the sizes
- $senha->setSize('70%', 40);
- $senha_confirm->setSize('70%', 40);
- $senha->style = 'height:35px; font-size:14px;float:left;border-bottom-left-radius: 0;border-top-left-radius: 0;';
- $senha_confirm->style = 'height:35px; font-size:14px;float:left;border-bottom-left-radius: 0;border-top-left-radius: 0;';
- $row=$table->addRow();
- $row->addCell( new TLabel('Nova senha') )->colspan = 2;
- $row->class='tformtitle';
- $senha->setLabel('Senha');
- $senha_confirm->setLabel('Senha_confirm');
- $senha->addValidation('Senha', new TRequiredValidator);
- $senha_confirm->addValidation('Senha_confirm', new TRequiredValidator);
- $locker = '<span style="float:left;width:35px;margin-left:45px;height:35px;" class="input-group-addon"><span class="glyphicon glyphicon-lock"></span></span>';
- $container1 = new TElement('div');
- $container1->add($locker);
- $container1->add($senha);
- $container1->add($user_id);
- $container2 = new TElement('div');
- $container2->add($locker);
- $container2->add($senha_confirm);
- $row=$table->addRow();
- $row->addCell($container1)->colspan = 2;
- $row=$table->addRow();
- $row->addCell($container2)->colspan = 2;
- // create an action button (save)
- $recuperar_button=new TButton('recovery');
- // define the button action
- $recuperar_button->setAction(new TAction(array($this, 'onReset')), _t('Save'));
- $recuperar_button->class = 'btn btn-success';
- $recuperar_button->style = 'font-size:18px;width:90%;padding:10px';
- $row=$table->addRow();
- $row->class = 'tformaction';
- $cell = $row->addCell( $recuperar_button );
- $cell->colspan = 2;
- $cell->style = 'text-align:center';
- $this->form->setFields(array($senha, $senha_confirm, $user_id, $recuperar_button));
- // add the form to the page
- parent::add($this->form);
- }
- public function onLogin()
- {
- TApplication::loadPage('LoginForm', '', $_REQUEST);
- }
- /**
- * Reset password
- */
- public function onReset()
- {
- try
- {
- $data = $this->form->getData('StdClass');
- $this->form->validate();
- //valida se senha e senha de confirmação foram digitadas iguais
- if ($data->senha != $data->senha_confirm)
- {
- new TMessage('error', 'Senhas não conferem, por favor digite as senhas iguais.');
- }
- else
- {
- // se passou pelas validações, instancia o usuário
- TTransaction::open('permission');
- $object = new SystemUser($data->user_id);
- if ($object)
- {
- // grava nova senha e limpa os outros campos, assim é garantido que o link é válido para apenas uma utilização
- $object->password = password_hash($data->senha, PASSWORD_DEFAULT);
- $object->reset_pass = NULL;
- $object->data_pass = NULL;
- $object->uid_pass = NULL;
- $object->store();
- new TMessage('info', 'Senha alterada com sucesso!', new TAction(array($this, 'onLogin')) );
- }
- else
- {
- new TMessage('error', _t('User not found'), new TAction(array($this, 'onLogin')) );
- }
- TTransaction::close();
- }
- }
- catch (Exception $e)
- {
- new TMessage('error',$e->getMessage());
- TTransaction::rollback();
- }
- }
- }
- ?>
Pessoal, qualquer melhoria ou sugestão, podem infomar.
Apenas para pontuar no último arquivo, salvei a senha usando password_hash, pois estou utilizando PHP superior a 5.5. Alterei toda a forma de encriptação no sistema.
Futuramente farei a parte de cadastro de usuário, utilizando uma lógica parecida, com confirmação por link enviado por email.
Abraços!
Muito legal, acho que deveria integrar ao templete, facilita nos próximos updates. Parabéns!
Parabéns pela iniciativa Henrique. Será muito útil para todos.
Alguem pode ajudar, informando de que forma devo chamar o formulario para digitar o email para recuperação de senha. Coloquei o seguinte codigo na pagina Login.html
<a href="index.php?class=LoginForm&method=onRecovery">[Esqueceu sua Senha?]</a></div>
<!-- /#wrapper -->
Esse metodo =onRecovery, faz ligação onde?
Amos ao invés de editar diretamente o Login.html, coloque um link na controladora de LoginForm logo depois do código do botão de entrar.
Esse método onRecovery está no próprio LoginForm
Henrique, boa noite. Muito legal a sua iniciativa, e funcionou perfeitamente, mas somente se eu acessar o sistema e chamar as telas manualmente. O link está na tela de login chamado o evento OnRecovery, que chama a tela SendEmailPasswordForm conforme abaixo:
Ao clicar no link, ele não abre a tela para digitar o e-mail e volta para a tela de login. O que esqueci de fazer?
Abs
Cleber Fosse
Cleber boa noite,
Fico feliz em ter ajudado. Esqueci de informar uma coisa no tutorial, pode ser o seu caso, é preciso adicionar as controladoras como páginas públicas no application.ini, pois as páginas podem estar caindo na checagem de permissão do framework. Veja se isso resolve.
Abraços!
Exatamente Henrique, era o que falta.
Muito obrigado pela ajuda.
Abraço e sucesso!!!
Henrique, boa noite.
Voltei a trabalhar no meu sistema e deparei com um problema ao recuperar a senha. Eu recebo o e-mail, faz a atualização no usuário corretamente, mas ao clicar no link não está indo para o formulário solicitar a nova senha e sim para a tela de login normal.
o meu link está da seguinte maneira:
Será que está correto?
Abs,
Cleber
Henrique, corrigindo link:
Cleber tudo bem?
Percebi que você está passando mais algumas outras variáveis GET no link, realizou alguma modificação? Me passa por gentileza qual deveria ser o link inteiro, não só a parte do index.php. Sugiro depurar o código no arquivo ResetPasswordForm e ver onde ele está caindo na variável $valido = false;
Henrique, boa noite.
Fiz uns testes e até comparei a versão do ResetPasswordForm com a sua e está igual. Vou colocar alguns TMessage para ver o resultado das variáveis.
Henrique, na url abaixo tem a mudança que eu fiz para testar se a classe estaria recebe-se ao menos o ID do usuário.
Poxa Vida Cara, Muiiito Show de bola obrigado pela colaboração
Cléber desculpa a demora! Ainda não consegui pegar o problema exato que vc está tendo, o ideal seria a depuração mesmo. Mas tem uma coisa que eu adianto, acabei realizando uma melhoria na montagem do link de reset de senha. Usar a variável $_SERVER['HTTP_HOST'] implica num problema de não conseguirmos pegar o endereço correto quando o sistema está em subpastas. Pra isso, criei mais um campo de preferências para setar a url_base do sistema. Ficando assim:
No seu exemplo, essa url_base você salvaria no banco como s2s.com.br/sid ou com HTTPS se for o caso. Recomendo bastante realizar essa alteração, isso ajuda a configurar URLs para diferentes ambientes e clientes.
Henrique, boa noite.
Fiz o ajuste para a montagem da URL e da maneira que fez acima (sem nenhuma alteração) e fez a montagem abaixo:
Estranho o porque a tela não é apresentada.
Obrigado pela ajuda. Vou tentar debugar amanhã,
Henrique, aproveitando: eu coloquei os dois fontes PHP na pasta app/control/public, está certo?
Outra coisa que identifiquei foi que o PublicView eu consigo carregar esse arquivo ao logar no sistema e digitando o nome do programa na URL. Fiz a mesma coisa para o ResetPasswordForm e ele dá erro de permissão de acesso.
Talvez essa informação possa ajudar.
Abs
Cleber Fosse
Cléber você pode estar com o mesmo problema que passou antes. Os arquivos não precisam necessariamente estar na pasta Control/public, mas precisam serem adicionadas no application.ini no public_classes
Henrique, boa noite.
O application.ini está OK.
Henrique,
[permission]
; Public classes, anyone (logged or not) has access
public_classes[] = PublicForm
public_classes[] = PublicView
public_classes[] = SendEmailPasswordForm
public_classes[] = ResetPasswordForm
Henrrique esto com problema ao eviar o email na tela de resetar a senha ao enviar o email me da este erro "Método SystemUser::newFromEmail() não encontrado"
O que sera que etou fazendo errado..
Segui todos os passos.
Pode me ajudar
Henrrique esto com problema ao eviar o email na tela de resetar a senha ao enviar o email me da este erro "Método SystemUser::newFromEmail() não encontrado"
O que sera que etou fazendo errado..
Segui todos os passos.
Pode me ajudar
Nilton é preciso implementar o método no modelo SystemUset.
Henrique,
Estou implementando a opção de recuperá com a sua dica, porém na hora de enviar o email -, da um erro de conexão,
error: connection failed;
Ficou muito bom, show de bola, testando o código acrescentei uma linha para redirecionar para a página de login depois de enviar o email.
na linha 115 alterei para:
e no arquivo LoginForm.php acrescentei
Amanhã vou tentar implementar um bloqueio de tela no form SendEmailPasswordForm enquanto estiver enviando o email. para o usuário não ficar clicando no botão " enviar ". Parabéns!
Miuller,
Tem como postar a solução, pois segui passo a passo, mas na hora que clico em emviar email da o sguinte erro :
Notice: Undefined index: url_base in /var/www/html/centinel/app/control/public/SendEmailPasswordForm.class.php on line 99
Fatal error: Uncaught Error: Class 'PHPMailerPHPMailerPHPMailer' not found in /var/www/html/centinel/app/lib/util/TMail.class.php:23 Stack trace: #0 /var/www/html/centinel/app/control/public/SendEmailPasswordForm.class.php(103): TMail->__construct() #1 [internal function]: SendEmailPasswordForm->onReset(Array) #2 /var/www/html/centinel/lib/adianti/control/TPage.php(51): call_user_func(Array, Array) #3 /var/www/html/centinel/lib/adianti/control/TPage.php(205): AdiantiControlTPage->run() #4 /var/www/html/centinel/lib/adianti/core/AdiantiCoreApplication.php(62): AdiantiControlTPage->show(Array) #5 /var/www/html/centinel/engine.php(34): AdiantiCoreAdiantiCoreApplication::run(true) #6 /var/www/html/centinel/engine.php(43): TApplication::run(true) #7 {main} thrown in /var/www/html/centinel/app/lib/util/TMail.class.php on line 23
Boa tarde Rubens, o email está enviando?, se a resposta for sim, na sua caixa de email, clicando no link teria que redirecionar para o seu projeto, tente substituir a variável urlbase.
Qualquer coisa posta ai
Miuller,
Então, fiz como indicado, porém agora da a mensagem que não esta encontrando a classe PHPMailer;
Observação isto esta acontecendo após atualizar para versão 5.0
Fatal error: Uncaught Error: Class 'PHPMailerPHPMailerPHPMailer' not found in /var/www/html/centinel/app/lib/util/TMail.class.php:23 Stack trace: #0 /var/www/html/centinel/app/control/public/SendEmailPasswordForm.class.php(104): TMail->__construct() #1 [internal function]: SendEmailPasswordForm->onReset(Array) #2 /var/www/html/centinel/lib/adianti/control/TPage.php(51): call_user_func(Array, Array) #3 /var/www/html/centinel/lib/adianti/control/TPage.php(205): AdiantiControlTPage->run() #4 /var/www/html/centinel/lib/adianti/core/AdiantiCoreApplication.php(62): AdiantiControlTPage->show(Array) #5 /var/www/html/centinel/engine.php(34): AdiantiCoreAdiantiCoreApplication::run(true) #6 /var/www/html/centinel/engine.php(43): TApplication::run(true) #7 {main} thrown in /var/www/html/centinel/app/lib/util/TMail.class.php on line 23
Boa tarde, observando a mensagem de erro observei que tem um problema na classe "/var/www/html/centinel/app/lib/util/TMail.class.php:23 ", tenho uma sugestão, faça um backup da classe TMail de seu projeto e crie um novo projeto, dai nesse novo projeto copia a classe TMail e substitui pela de seu projeto (a que vc fez o backup), e vê se soluciona o problema.
Miuller,
Não funcionou funcionou, já tinha feito isto antes.
Rubens, tente criar um novo projeto e adicione as p áginas deste exemplo no novo projeto, só para testar mesmo, dando uma olhada no projeto, notei que o envio em localhost tava funcionando mas quando coloquei o projeto no servidor estava dando um erro no envio, talvez seja o seu caso, vou postar as modificações que realizei.
Testei e deu tudo certo, se vc não conseguir me fala no miuller.de.faria@gmail.com e te dou uma mão, flw
Essa modificação acima é no arquivo SendEmailPassordForm, agora fiz outra modificação no arquivo ResetPaswordForm, conforme abaixo, atentar para a linha 29, pois ele verifica se o parâmetro email é igual ao da base de dados, essa é uma segurança a mais.
Muito obrigado!!! Muito útil, também não entendi porque não vem no admin padrão do framework!
Funcionou perfeitamente, vale ressaltar alguns detalhes óbvios para alguns porém não para todos:
É necessário configurar corretamente o formulário de preferencias do sistema, e para tanto, deve-se ter uma conta de email valida na hospedagem. não consegui colocar pra funcionar com o gmail...
Na classe ResetPasswordForm Alterar o salvamento da nova senha para md5 pois como o amigo citou, ele reformulou o sistema de login dele e o login padrão do framework utiliza md5 para geração e verificação das senhas.
linha : 149
$object->password = password_hash($data->senha, PASSWORD_DEFAULT);
para :
$object->password = md5($data->senha);
feito isso tudo funcionou perfeitamente!
Henrique, boa noite.
Como ficaria a parte abaixo do código usando a versão 5 do framework?
// $recupera = new TActionLink( _t('Forgot your password?'), new TAction(array($this, 'onRecovery')) );
// $recupera->style = 'font-size:16px;width:90%;padding:10px';
// $row = $table->addRow();
// $cell = $row->addCell( $recupera );
Abs
Cleber
No meu caso, eu coloquei tudo em um panel, e abaixo coloquei o link para o método onRecovery
Caso alguém precise dessa solução funcionando, dentro das minhas limitações de tempo eu passo todo o código funcionando em conjunto com o Admin padrão do adianti, já que estou muito grato por ter pego a solução aqui que me poupou muito trabalho e estudo, ajudarei com prazer outros usuários!
Davidson,
Se puder disponibilizar ó código, serei muito grato.
Segue meu e-mail :rubensbispo29@gmail.com
Vou mandar por e-mail e colocar aqui até amanhã de manhã! Mas adianto que a única modificação necessária foi a que postei acima na geração da nova senha em md5 , até lá, tente seguir o tutorial desde o início do post, caso não consiga, terá de estudar um pouco mais o framework.