суббота, 22 декабря 2012 г.

Управляем доступом - 4. Восстанавливаем пароль.

Пост посвящается всем пережившим вчерашний конец света ;)

Мой проект (я его упоминал уже как-то, если в двух словах - он про спортивные соревнования) развивается медленно (потому что спросом не пользуется) и спросом не пользуется (потому что развивается медленно). Но раз в год он точно нужен и востребован, и в прошлый раз там даже какое-то количество людей зарегистрировалось. А раз приближается время, когда все эти люди потянутся на мой сайт опять, да не просто потянутся, а обнаружат, что не могут вспомнить свой пароль, то и эту проблему как-то придется решать. А как? Средствами апекса - никак (пока никак, но об этом позже).



Я забыл пароль :(

Ну с кем не бывает. Если забыл, надо или вспоминать, или восстанавливать. А как вспоминать, если вводил его всего один раз, и то год назад? Значит, без нас забывчивому пользователю не обойтись! Идем на страницу 101 (стандарная страница логина), добавляем там элемент с типом Display Only и дефолтным значением

<u><a href="f?p=&APP_ID.:102:&APP_SESSION.:">Забыли пароль?</a></u>

В разделе Label удаляем текст ярлыка и выбираем Template "No Label".
К чему такой огород? В принципе, ссылку можно создать, просто добавив в Region Source региона вышеприведенный код. Но тогда ссылка будет нарисована просто в углу, а в нашем случае она будет выставлена аккуратно под полем для ввода пароля и выровнена по левому краю:


Теперь надо создать страницу с номером 102 (можете выбрать себе любой другой номер, какой больше нравится, только ссылку не забудьте поправить), на которой пользователь сможет попытаться восстановить свой пароль.

Страница восстановления пароля

Страницу восстановления я решил сделать с ajax'ом. Что самое странное, совсем без извращений обойти не удалось. Пожелания мои к странице были следующие:
  • пользователь для идентификации может ввести логин или адрес электронной почты от своей учетной записи
  • в случае, когда логин или почта найдены, сбросить пароль и выслать на указанный в учетной записи адрес письмо с временным паролем
  • если не найден логин/пароль - сообщить об этом пользователю
  • запрос делать с помощью ajax, выводить на страницу сообщение о результате.
Попутно обнаружилась странная особенность кнопок. В свойствах кнопки в разделе Button Display Attributes есть свойство Style, которое может иметь три значения: HTML button, Template Based Button и Image. При этом только в последнем случае кнопка может вызвать javascript-функцию. Почему так устроено, я, если честно, пока не знаю. Но решение не очень удобное, потому что если надо вызвать javascript, то кнопка должна быть только в виде картинки, а это создает трудности с дизайном, потому что сменив стиль страницы, можно получить вырвиглазную кнопку прямо посередине.
Зато есть возможность это обойти: создаем обычный Display Only элемент страницы, очищаем у него Label, в HTML Form Element Attributes вставляем строчку onclick="recover_pwd()" (это будет наша javascript-функция для восстановления пароля), а Defualt Value - вот такое:

<button type="button">Восстановить</button>

И получаем кнопку, которая выглядит как HTML Button и при этом вызывает javascript.

Код

Javascript-вызов подробно описывать не буду - там почти все то же самое, что и здесь. Только в этот раз будем передавать два параметра вместо одного (еще напомню, что ajax в апекс-приложениях с авторизацией и без работает немного по-разному).
Теперь код для смены пароля:

create or replace function "RECOVER_PWD"
(p_login  VARCHAR2,
 p_email  VARCHAR2) return varchar2
is
    u_id    number;
    u_mail  varchar2(255);
    by_login boolean;
    by_email boolean;
    new_pwd  varchar2(255) := '123456';  -- это наш временный пароль
    new_pwde varchar2(255);
begin
  if p_login is not null then
   -- находим ID пользователя и email по указанному логину:  
   by_login := true;
     select id, email
       into u_id, u_mail
       from users
      where login = UPPER(p_login);
 
     -- если у пользователя не указан email - сообщаем об этом
     -- (остальные варианты обработаем позже):
     if u_mail is null then
         return('Вы не указали почтового адреса при регистрации. ' || 
         'Для восстановления учетной записи свяжитесь с администрацией.');
     end if;

  elsif p_email is not null then
   -- находим ID пользователя и email по указанному почтовому адресу:  
     by_email := true;
     select id
       into u_id
       from users
      where email = LOWER(p_email);
end if;

  -- если досюда добрались, значит, нашли указанного пользователя и его адрес.
  -- получаем хэш временного пароля:
  new_pwde:=dbms_obfuscation_toolkit.md5(input_string => new_pwd);
  
  -- хэш - в таблицу:
  update users set pwd = new_pwde where id = u_id;
  -- пароль - пользователю на почту:
  send_recovery_info(nvl(p_email, u_mail), new_pwd);

-- и отчитаемся:
return('Временный пароль выслан на почтовый адрес, указанный вами при регистрации.');

exception
  -- в случае, если селекты ничего не нашли, определяем с помощью 
  -- переменных by_login и by_email по какому полю искали:
  when no_data_found then
    if by_login then
       return('Нет ни одной учетной записи с таким именем пользователя');
    end if;
    if by_email then
       return('Нет ни одной учетной записи с таким адресом электроной почты');
    end if;
  when too_many_rows then
    -- А это уже караул, кто ж такой бардак в таблице развел???
    -- Сделайте уже что-нибудь!!! Только без меня...
end;

Соответственно, Application Process для этого вызова выглядит так:

begin
htp.prn(recover_pwd(:P102_LOGIN, :P102_EMAIL));
end;

Мы передаем в нашу основную функцию логин и адрес со страницы восстановления пароля, а возвращаемый результат  функции - текстовое описание того, что именно произошло - отправляем обратно на страницу, чтобы пользователь знал, что его запрос не исчез в пучине интернета, а как-то обработался. В случае, если все прошло успешно - пользователю на найденный адрес отправляется уведомление (функция send_recovery_info с 43-й строки):

create or replace procedure "SEND_RECOVERY_INFO"
(p_email  VARCHAR2, p_new_pwd varchar2)
is
    l_body varchar2(32767);
    res integer;
begin
    l_body := 'Ваш временный пароль: ' || p_new_pwd || '. Поменяйте его как можно быстрее.';
    res := APEX_MAIL.SEND(p_email, 'ваш@обратный.адрес', l_body, NULL, 'Восстановление пароля');
end;

Здесь мы используем  процедуру SEND из пакета APEX_MAIL - отправить письмо из апекса проще простого, как видите. Помните только, что вторым параметром придется поставить валидный обратный email - рассылать спам вам апекс не даст. Хотя, когда я отправил тестовое письмо самому себе, gmail поместил его в спам и дополнительно сообщил, что письмо, возможно, отправлено не с того адреса, который указан в поле "от кого" - так что хоть спам вы и не рассылаете, но ваши адресаты его получают ;)

 А тем временем на странице у пользователя...

...долго сказка сказывается, да быстро код выполняется. Чтобы увидеть ответ, надо его куда-то вывести. Тут либо у апекса некоторые проблемы, либо я что-то не так понял :( Стандартное апексовое окошко вверху страницы (например, такое зелененькое вверху, появляется, когда вы редактируете свойства какой-нибудь страницы) не приспособлено для ajax - его надо заполнять значением перед показом страницы. Можно сделать просто вызов функции alert(), но тогда браузер вам покажет модальное окно с сообщением, а модальные окна без особой необходимости - это ЗлоЪ™ в чистом виде, вам это любой специалист по юзабилити подтвердит. Я решил сделать так: создал элемент с названием P102_RESPONSE и разместил его ниже. Тут опять возник сюрприз... Чтобы поменять значение элемента страницы, я  раньше использовал функцию html_GetElement('elementName').value, но тут оказалось, что таким способом можно менять только значения полей для ввода, а для Display Only элементов надо пользоваться функцией $s (вот кстати документация по всем функциям, чтобы долго не искать):

function recover_pwd() { 
/*Ну тут мы посылаем запрос серверу, это все уже умеют делать, получаем ответ... */
var gReturn = get.get();

/* и показываем пользователю: */
$s('P102_RESPONSE', gReturn);
}

Вот так можно сделать восстановление пароля. Вроде ничего существенного не забыл...

Счастье для всех, даром, и пусть никто не уйдет обиженный

Если вам не хочется делать все это самостоятельно, вы можете пойти на ту самую голосовалку за Feature Request'ы на сайте Oracle и проголосовать за то, чтобы соответствующую функциональность добавили в APEX (даже если вам хочется сделать это самостоятельно, все равно сходите и проголосуйте). Фича зарегистрирована под кодом ADZ6. Будем надеяться, что наши голоса услышат.

Комментариев нет:

Отправить комментарий