diff --git a/cms-safe.ecf b/cms-safe.ecf index d1c2b3c..1fa90e4 100644 --- a/cms-safe.ecf +++ b/cms-safe.ecf @@ -28,6 +28,7 @@ + diff --git a/cms.ecf b/cms.ecf index a41c820..3788249 100644 --- a/cms.ecf +++ b/cms.ecf @@ -29,6 +29,7 @@ + diff --git a/examples/demo/demo-safe.ecf b/examples/demo/demo-safe.ecf index f7a4714..2414c36 100644 --- a/examples/demo/demo-safe.ecf +++ b/examples/demo/demo-safe.ecf @@ -18,6 +18,7 @@ + diff --git a/examples/demo/site/config/modules/login/account_activation.html b/examples/demo/site/config/modules/login/account_activation.html new file mode 100644 index 0000000..0ab4c4f --- /dev/null +++ b/examples/demo/site/config/modules/login/account_activation.html @@ -0,0 +1,18 @@ + + + + + Activation + + + + + +

Thank you for registering at ROC CMS

+ +

To complete your registration, please click on this link to activate your account:

+ +

$link

+

Thank you for joining us.

+ + diff --git a/examples/demo/site/config/modules/login/account_new_password.html b/examples/demo/site/config/modules/login/account_new_password.html new file mode 100644 index 0000000..dfcd355 --- /dev/null +++ b/examples/demo/site/config/modules/login/account_new_password.html @@ -0,0 +1,17 @@ + + + + + New Password + + + + + +

You have required a new password at ROC CMS

+ +

To complete your request, please click on this link to genereate a new password:

+ +

$link

+ + \ No newline at end of file diff --git a/examples/demo/site/config/modules/login/account_re_activation.html b/examples/demo/site/config/modules/login/account_re_activation.html new file mode 100644 index 0000000..95aebb2 --- /dev/null +++ b/examples/demo/site/config/modules/login/account_re_activation.html @@ -0,0 +1,18 @@ + + + + + New Activation + + + + + +

You have request a new activation token atROC CMS

+ +

To complete your registration, please click on this link to activate your account:

+ +

$link

+

Thank you for joining us.

+ + \ No newline at end of file diff --git a/examples/demo/site/config/modules/login/account_welcome.html b/examples/demo/site/config/modules/login/account_welcome.html new file mode 100644 index 0000000..0ea987c --- /dev/null +++ b/examples/demo/site/config/modules/login/account_welcome.html @@ -0,0 +1,13 @@ + + + + + Welcome + + + + +

Welcome toROC CMS

+

Thank you for joining us.

+ + \ No newline at end of file diff --git a/examples/demo/site/config/modules/login/login.json b/examples/demo/site/config/modules/login/login.json new file mode 100644 index 0000000..2f3e155 --- /dev/null +++ b/examples/demo/site/config/modules/login/login.json @@ -0,0 +1,8 @@ +{ + "email": "webmaster@example.com", + "subjet_register": "Thank you for regitering with us, activate account", + "subjet_activate": "New account ativation token", + "subjet_password": "Password Recovery!!!", + "subjet_oauth": "Welcome", + "smtp": "127.0.0.1" +} diff --git a/examples/demo/site/config/modules/login/oauth2_gmail.json b/examples/demo/site/config/modules/login/oauth2_gmail.json new file mode 100644 index 0000000..8cd35ef --- /dev/null +++ b/examples/demo/site/config/modules/login/oauth2_gmail.json @@ -0,0 +1,7 @@ +{ + "api_secret":"ADD_YOUR_SECRET_KEY", + "api_key":"ADD_YOUR_PUBLIC_KEY", + "scope": "email", + "api_revoke":"https://accounts.google.com/o/oauth2/revoke?token=$ACCESS_TOKEN", + "protected_resource_url":"https://www.googleapis.com/plus/v1/people/me" +} diff --git a/examples/demo/site/scripts/oauth2_consumers.sql b/examples/demo/site/scripts/oauth2_consumers.sql new file mode 100644 index 0000000..ae0d1da --- /dev/null +++ b/examples/demo/site/scripts/oauth2_consumers.sql @@ -0,0 +1,18 @@ + +CREATE TABLE `oauth2_consumers`( + `cid` INTEGER PRIMARY KEY NOT NULL CHECK(`cid`>=0), + `name` VARCHAR(255) NOT NULL, + `api_secret` TEXT NOT NULL, + `api_key` TEXT NOT NULL, + `scope` VARCHAR (100) NOT NULL, + `protected_resource_url` VARCHAR (255) NOT NULL, + `callback_name` VARCHAR(255) NOT NULL, + `extractor` VARCHAR(50) NOT NULL, + `authorize_url` VARCHAR (255) NOT NULL, + `endpoint` VARCHAR (255) NOT NULL, + CONSTRAINT `cid` + UNIQUE(`cid`), + CONSTRAINT `name` + UNIQUE(`name`) + ); + diff --git a/examples/demo/site/scripts/oauth2_consumers_initialize.sql b/examples/demo/site/scripts/oauth2_consumers_initialize.sql new file mode 100644 index 0000000..69cd233 --- /dev/null +++ b/examples/demo/site/scripts/oauth2_consumers_initialize.sql @@ -0,0 +1,7 @@ + -- Change the values `TO_COMPLETE` based on your API. + -- API SECTET KEY AND API PUBLIC KEY + +INSERT INTO `oauth2_consumers` ("name", "api_secret", "api_key", "scope", "protected_resource_url", "callback_name", "extractor", "authorize_url", "endpoint") +VALUES ("google", 'TO-COMPLETE', 'TO-COMPLETE', 'email', 'https://www.googleapis.com/plus/v1/people/me', "callback_google", "json","https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI","https://accounts.google.com/o/oauth2/token"); +INSERT INTO "oauth2_consumers" ("name", "api_secret", "api_key", "scope", "protected_resource_url", "callback_name", "extractor", "authorize_url", "endpoint" ) +VALUES ("facebook", 'TO-COMPLETE', 'TO-COMPLETE', 'email', 'https://graph.facebook.com/me', "callback_facebook","text","https://www.facebook.com/dialog/oauth?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI","https://graph.facebook.com/oauth/access_token"); diff --git a/examples/demo/site/scripts/oauth2_template.sql b/examples/demo/site/scripts/oauth2_template.sql new file mode 100644 index 0000000..f53ee67 --- /dev/null +++ b/examples/demo/site/scripts/oauth2_template.sql @@ -0,0 +1,10 @@ + +CREATE TABLE :table_name ( + `uid` INTEGER PRIMARY KEY NOT NULL CHECK(`uid`>=0), + `access_token` TEXT NOT NULL, + `created` DATETIME NOT NULL, + `details` TEXT NOT NULL, + CONSTRAINT `uid` + UNIQUE(`uid`) + ); + diff --git a/examples/demo/site/scripts/user.sql b/examples/demo/site/scripts/user.sql index 64c2f9e..6e286af 100644 --- a/examples/demo/site/scripts/user.sql +++ b/examples/demo/site/scripts/user.sql @@ -30,3 +30,19 @@ CREATE TABLE `role_permissions`( `module` VARCHAR(255) ); +CREATE TABLE "users_activations" ( + "aid" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("aid" >= 0), + "token" VARCHAR(255) NOT NULL, + "uid" INTEGER NOT NULL CHECK ("uid" >= 0), + "created" DATETIME NOT NULL, + CONSTRAINT "token" UNIQUE ("token") +); + +CREATE TABLE "users_password_recovery" ( + "aid" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("aid" >= 0), + "token" VARCHAR(255) NOT NULL, + "uid" INTEGER NOT NULL CHECK ("uid" >= 0), + "created" DATETIME NOT NULL, + CONSTRAINT "token" UNIQUE ("token") +); + diff --git a/examples/demo/site/themes/bootstrap/assets/js/roc_auth.js b/examples/demo/site/themes/bootstrap/assets/js/roc_auth.js index 06f0731..75382ef 100644 --- a/examples/demo/site/themes/bootstrap/assets/js/roc_auth.js +++ b/examples/demo/site/themes/bootstrap/assets/js/roc_auth.js @@ -1,7 +1,8 @@ var ROC_AUTH = ROC_AUTH || { }; -var loginURL = "/login"; -var logoutURL = "/logoff"; +var loginURL = "/basic_auth_login"; +var logoutURL = "/basic_auth_logoff"; + var userAgent = navigator.userAgent.toLowerCase(); var firstLogIn = true; @@ -305,3 +306,16 @@ ROC_AUTH.create_form = function() { }; +var password = document.getElementById("password") + , confirm_password = document.getElementById("confirm_password"); + +ROC_AUTH.validatePassword =function(){ + if(password.value != confirm_password.value) { + confirm_password.setCustomValidity("Passwords Don't Match"); + } else { + confirm_password.setCustomValidity(''); + } +} + +password.onchange = ROC_AUTH.validatePassword(); +confirm_password.onkeyup = ROC_AUTH.validatePassword; \ No newline at end of file diff --git a/examples/demo/site/themes/bootstrap/modules/login/templates/block_login.tpl b/examples/demo/site/themes/bootstrap/modules/login/templates/block_login.tpl new file mode 100644 index 0000000..dda41b5 --- /dev/null +++ b/examples/demo/site/themes/bootstrap/modules/login/templates/block_login.tpl @@ -0,0 +1,34 @@ +
+ {unless isset="$user"} +

Login or Register

+
+
+
+
+ + +
+ +
+ + +
+ + +
+
+
+
+ +
+
+ {foreach item="item" from="$oauth_consumers"} + Login with {$item/}
+ {/foreach} +
+ {/unless} +
\ No newline at end of file diff --git a/examples/demo/site/themes/bootstrap/modules/login/templates/block_new_password.tpl b/examples/demo/site/themes/bootstrap/modules/login/templates/block_new_password.tpl new file mode 100644 index 0000000..ef5407f --- /dev/null +++ b/examples/demo/site/themes/bootstrap/modules/login/templates/block_new_password.tpl @@ -0,0 +1,16 @@ +
+
+
+ Require new password +
+ + + {if isset="$error_email"} + {$error_email/}
+ {/if} +
+
+ +
+
+
diff --git a/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_password.tpl b/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_password.tpl new file mode 100644 index 0000000..0ec7a7c --- /dev/null +++ b/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_password.tpl @@ -0,0 +1,3 @@ +
+

We have send you a new token code, check your email to generate a new password

+
diff --git a/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_reactivate.tpl b/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_reactivate.tpl new file mode 100644 index 0000000..09e7206 --- /dev/null +++ b/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_reactivate.tpl @@ -0,0 +1,3 @@ +
+

We have send you a new activation code, check your email to activate your account.

+
diff --git a/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_register.tpl b/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_register.tpl new file mode 100644 index 0000000..d59f75a --- /dev/null +++ b/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_register.tpl @@ -0,0 +1,3 @@ +
+

Thanks for register, check your email to activate your account.

+
diff --git a/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_reset.tpl b/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_reset.tpl new file mode 100644 index 0000000..9ccecfb --- /dev/null +++ b/examples/demo/site/themes/bootstrap/modules/login/templates/block_post_reset.tpl @@ -0,0 +1,3 @@ +
+

You new password has been saved!

+
diff --git a/examples/demo/site/themes/bootstrap/modules/login/templates/block_reactivate.tpl b/examples/demo/site/themes/bootstrap/modules/login/templates/block_reactivate.tpl new file mode 100644 index 0000000..bef65c0 --- /dev/null +++ b/examples/demo/site/themes/bootstrap/modules/login/templates/block_reactivate.tpl @@ -0,0 +1,19 @@ +
+
+
+ Reactivate Form +
+ + + {if isset="$error_email"} + {$error_email/}
+ {/if} +
+ {if isset="$is_active"} + {$is_active/}
+ {/if} +
+ +
+
+
diff --git a/examples/demo/site/themes/bootstrap/modules/login/templates/block_register.tpl b/examples/demo/site/themes/bootstrap/modules/login/templates/block_register.tpl new file mode 100644 index 0000000..58a4872 --- /dev/null +++ b/examples/demo/site/themes/bootstrap/modules/login/templates/block_register.tpl @@ -0,0 +1,28 @@ +
+
+
+ Register Form +
+ + + {if isset="$error_name"} + {$error_name/}
+ {/if} +
+
+ + +
+
+ + + {if isset="$error_email"} + {$error_email/}
+ {/if} +
+ + + +
+
+
diff --git a/examples/demo/site/themes/bootstrap/modules/login/templates/block_reset_password.tpl b/examples/demo/site/themes/bootstrap/modules/login/templates/block_reset_password.tpl new file mode 100644 index 0000000..b99289d --- /dev/null +++ b/examples/demo/site/themes/bootstrap/modules/login/templates/block_reset_password.tpl @@ -0,0 +1,28 @@ +
+
+
+ Generate New Password Form +
+ + + {if isset="$error_token"} + {$error_token/}
+ {/if} +
+
+ + +
+
+ + +
+ + + {if isset="$error_password"} + {$error_password/}
+ {/if} + +
+
+
diff --git a/examples/demo/site/themes/bootstrap/page.tpl b/examples/demo/site/themes/bootstrap/page.tpl index 9361fde..2223976 100644 --- a/examples/demo/site/themes/bootstrap/page.tpl +++ b/examples/demo/site/themes/bootstrap/page.tpl @@ -61,7 +61,7 @@ {unless isempty="$page_title"}

{$page_title/}

{/unless} {$page.region_content/} - + diff --git a/examples/demo/src/ewf_roc_server_execution.e b/examples/demo/src/ewf_roc_server_execution.e index 22a937a..6a48db1 100644 --- a/examples/demo/src/ewf_roc_server_execution.e +++ b/examples/demo/src/ewf_roc_server_execution.e @@ -59,6 +59,10 @@ feature -- CMS setup m.enable a_setup.register_module (m) + create {CMS_AUTHENTICATION_MODULE} m.make + m.enable + a_setup.register_module (m) + create {BASIC_AUTH_MODULE} m.make if not a_setup.module_with_same_type_registered (m) then m.enable diff --git a/library/model/src/user/cms_user.e b/library/model/src/user/cms_user.e index 5abb436..713ca65 100644 --- a/library/model/src/user/cms_user.e +++ b/library/model/src/user/cms_user.e @@ -27,6 +27,7 @@ feature {NONE} -- Initialization initialize ensure name_set: name = a_name + status_not_active: status = not_active end make_with_id (a_id: INTEGER_64) @@ -38,11 +39,13 @@ feature {NONE} -- Initialization initialize ensure id_set: id = a_id + status_not_active: status = not_active end initialize do create creation_date.make_now_utc + mark_not_active end feature -- Access @@ -71,6 +74,13 @@ feature -- Access last_login_date: detachable DATE_TIME -- User last login. + status: INTEGER + -- Associated status for the current user. + -- default: not_active + -- active + -- trashed + + feature -- Roles roles: detachable LIST [CMS_USER_ROLE] @@ -118,6 +128,12 @@ feature -- Status report Result := other /= Void and then id = other.id end + is_active: BOOLEAN + -- is the current user active? + do + Result := status = {CMS_USER}.active + end + feature -- Change element set_id (a_id: like id) @@ -225,6 +241,52 @@ feature -- Change element: data end end +feature -- Status change + + mark_not_active + -- Set status to not_active + do + set_status (not_active) + ensure + status_not_active: status = not_active + end + + mark_active + -- Set status to active. + do + set_status (active) + ensure + status_active: status = active + end + + mark_trashed + -- Set status to trashed. + do + set_status (trashed) + ensure + status_trash: status = trashed + end + + set_status (a_status: like status) + -- Assign `status' with `a_status'. + do + status := a_status + ensure + status_set: status = a_status + end + + +feature -- User status + + not_active: INTEGER = 0 + -- The user is not active. + + active: INTEGER = 1 + -- The user is active + + Trashed: INTEGER = -1 + -- The user is trashed (soft delete), ready to be deleted/destroyed from storage. + invariant id_or_name_set: id > 0 or else not name.is_whitespace diff --git a/library/persistence/mysql/scripts/core.sql b/library/persistence/mysql/scripts/core.sql new file mode 100644 index 0000000..366ccbd --- /dev/null +++ b/library/persistence/mysql/scripts/core.sql @@ -0,0 +1,30 @@ +BEGIN; + +CREATE TABLE `logs` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `category` VARCHAR(255) NOT NULL, + `level` int(11) NOT NULL, + `uid` int(11) DEFAULT NULL, + `message` text NOT NULL, + `info` text, + `link` text, + `date` datetime NOT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `custom_values` ( + `type` VARCHAR(255) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `value` VARCHAR(255) NOT NULL +); + +CREATE TABLE `path_aliases` ( + `pid` int(11) NOT NULL AUTO_INCREMENT, + `source` varchar(255) NOT NULL, + `alias` varchar(255) NOT NULL, + `lang` varchar(12) DEFAULT NULL, + PRIMARY KEY (`pid`) +); + +COMMIT; + diff --git a/library/persistence/mysql/scripts/node.sql b/library/persistence/mysql/scripts/node.sql new file mode 100644 index 0000000..a4a053e --- /dev/null +++ b/library/persistence/mysql/scripts/node.sql @@ -0,0 +1,24 @@ +BEGIN; + +CREATE TABLE nodes ( + nid INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL CHECK( nid >=0), + revision INTEGER, + type TEXT NOT NULL, + title VARCHAR(255) NOT NULL, + summary TEXT, + content MEDIUMTEXT NOT NULL, + format VARCHAR(255), + author INTEGER, + publish DATETIME, + created DATETIME NOT NULL, + changed DATETIME NOT NULL, + status INTEGER +); + +CREATE TABLE page_nodes( + nid INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL CHECK( nid >=0), + revision INTEGER, + parent INTEGER +); + +COMMIT; diff --git a/library/persistence/mysql/scripts/user.sql b/library/persistence/mysql/scripts/user.sql new file mode 100644 index 0000000..e99072a --- /dev/null +++ b/library/persistence/mysql/scripts/user.sql @@ -0,0 +1,66 @@ +BEGIN; + +CREATE TABLE `users` ( + `uid` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + `password` varchar(100) NOT NULL, + `salt` varchar(100) NOT NULL, + `email` varchar(250) NOT NULL, + `status` int(11) DEFAULT NULL, + `created` datetime NOT NULL, + `signed` datetime DEFAULT NULL, + CHECK (`uid` >= 0), + PRIMARY KEY (`uid`), + UNIQUE KEY `name` (`name`) +); + +CREATE TABLE `roles` ( + `rid` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + CHECK (`rid` >= 0), + PRIMARY KEY (`rid`), + UNIQUE KEY `name` (`name`) +); + + +CREATE TABLE `users_roles` ( + `uid` int(11) NOT NULL, + `rid` int(11) NOT NULL, + CHECK (`uid` >= 0), + CHECK (`rid` >= 0) +); + +CREATE TABLE `role_permissions` ( + `rid` int(11) NOT NULL, + `permission` varchar(255) NOT NULL, + `module` varchar(255) DEFAULT NULL, + CHECK (`rid` >= 0) +); + + +CREATE TABLE `users_activations` ( + `aid` int(11) NOT NULL AUTO_INCREMENT, + `token` varchar(255) NOT NULL, + `uid` int(11) NOT NULL, + `created` datetime NOT NULL, + CHECK (`aid` >= 0), + CHECK (`uid` >= 0), + PRIMARY KEY (`aid`), + UNIQUE KEY `token` (`token`) +); + + +CREATE TABLE `users_password_recovery` ( + `aid` int(11) NOT NULL AUTO_INCREMENT, + `token` varchar(255) NOT NULL, + `uid` int(11) NOT NULL, + `created` datetime NOT NULL, + CHECK (`aid` >= 0), + CHECK (`uid` >= 0), + PRIMARY KEY (`aid`), + UNIQUE KEY `token` (`token`) +); + + + +COMMIT; \ No newline at end of file diff --git a/modules/basic_auth/basic_auth_module.e b/modules/basic_auth/basic_auth_module.e index 515cac7..0d9d649 100644 --- a/modules/basic_auth/basic_auth_module.e +++ b/modules/basic_auth/basic_auth_module.e @@ -108,14 +108,14 @@ feature -- Hooks local lnk: CMS_LOCAL_LINK do - if attached a_response.current_user (a_response.request) as u then - create lnk.make (u.name + " (Logout)", "basic_auth_logoff?destination=" + a_response.request.request_uri) - else - create lnk.make ("Login", "basic_auth_login?destination=" + a_response.request.request_uri) - end +-- if attached a_response.current_user (a_response.request) as u then +-- create lnk.make (u.name + " (Logout)", "basic_auth_logoff?destination=" + a_response.request.request_uri) +-- else +-- create lnk.make ("Login", "basic_auth_login?destination=" + a_response.request.request_uri) +-- end -- if not a_menu_system.primary_menu.has (lnk) then - lnk.set_weight (99) - a_menu_system.primary_menu.extend (lnk) +-- lnk.set_weight (99) +-- a_menu_system.primary_menu.extend (lnk) -- end end diff --git a/modules/basic_auth/handler/basic_auth_logoff_handler.e b/modules/basic_auth/handler/basic_auth_logoff_handler.e index 40ca330..5e8a667 100644 --- a/modules/basic_auth/handler/basic_auth_logoff_handler.e +++ b/modules/basic_auth/handler/basic_auth_logoff_handler.e @@ -55,21 +55,62 @@ feature -- HTTP Methods else create {GENERIC_VIEW_CMS_RESPONSE} l_page.make (req, res, api) unset_current_user (req) - l_page.set_status_code ({HTTP_STATUS_CODE}.found) -- Note: can not use {HTTP_STATUS_CODE}.unauthorized for redirection - if attached {WSF_STRING} req.query_parameter ("destination") as l_uri then - l_url := req.absolute_script_url (l_uri.url_encoded_value) - else - l_url := req.absolute_script_url ("") - end + l_page.set_status_code ({HTTP_STATUS_CODE}.unauthorized) -- Note: can not use {HTTP_STATUS_CODE}.unauthorized for redirection + l_url := req.absolute_script_url ("") i := l_url.substring_index ("://", 1) if i > 0 then -- Note: this is a hack to have the logout effective on various browser -- (firefox requires this). l_url.replace_substring ("://_logout_basic_auth_@", i, i + 2) end - l_page.set_redirection (l_url) + if + attached req.http_user_agent as l_user_agent and then + browser_name (l_user_agent).is_case_insensitive_equal_general ("Firefox") + then + -- Set status to refirect + -- and redirect to the host page. + l_page.set_status_code ({HTTP_STATUS_CODE}.found) + l_page.set_redirection (l_url) + end l_page.execute end end + + browser_name (a_user_agent: READABLE_STRING_8): READABLE_STRING_32 + -- Browser name. + -- Must contain Must not contain + -- Firefox Firefox/xyz Seamonkey/xyz + -- Seamonkey Seamonkey/xyz + -- Chrome Chrome/xyz Chromium/xyz + -- Chromium Chromium/xyz + -- Safari Safari/xyz Chrome/xyz + -- Chromium/xyz + -- Opera OPR/xyz [1] + -- Opera/xyz [2] + -- Internet Explorer ;MSIE xyz; Internet Explorer doesn't put its name in the BrowserName/VersionNumber format + + do + if + a_user_agent.has_substring ("Firefox") and then + not a_user_agent.has_substring ("Seamonkey") + then + Result := "Firefox" + elseif a_user_agent.has_substring ("Seamonkey") then + Result := "Seamonkey" + elseif a_user_agent.has_substring ("Chrome") and then not a_user_agent.has_substring ("Chromium")then + Result := "Chrome" + elseif a_user_agent.has_substring ("Chromium") then + Result := "Chromiun" + elseif a_user_agent.has_substring ("Safari") and then not (a_user_agent.has_substring ("Chrome") or else a_user_agent.has_substring ("Chromium")) then + Result := "Safari" + elseif a_user_agent.has_substring ("OPR") or else a_user_agent.has_substring ("Opera") then + Result := "Opera" + elseif a_user_agent.has_substring ("MSIE") or else a_user_agent.has_substring ("Trident")then + Result := "Internet Explorer" + else + Result := "Unknown" + end + end + end diff --git a/modules/login/cms_authentication_constants.e b/modules/login/cms_authentication_constants.e new file mode 100644 index 0000000..6278ed8 --- /dev/null +++ b/modules/login/cms_authentication_constants.e @@ -0,0 +1,13 @@ +note + description: "Summary description for {CMS_AUTHENTICATION_CONSTANTS}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_AUTHENTICATION_CONSTANTS + +feature -- Access + + oauth_session: STRING = "EWF_ROC_OAUTH_TOKEN_" + +end diff --git a/modules/login/cms_authentication_email_service_parameters.e b/modules/login/cms_authentication_email_service_parameters.e new file mode 100644 index 0000000..20746f2 --- /dev/null +++ b/modules/login/cms_authentication_email_service_parameters.e @@ -0,0 +1,268 @@ +note + description: "Summary description for {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS + +inherit + EMAIL_SERVICE_PARAMETERS + +create + make + +feature {NONE} -- Initialization + + make (a_cms_api: CMS_API) + local + utf: UTF_CONVERTER + l_site_name: READABLE_STRING_8 + s: detachable READABLE_STRING_32 + l_contact_email, l_subject_register, l_subject_activate, l_subject_password, l_subject_oauth: detachable READABLE_STRING_8 + do + setup := a_cms_api.setup + -- Use global smtp setting if any, otherwise "localhost" + smtp_server := utf.escaped_utf_32_string_to_utf_8_string_8 (a_cms_api.setup.text_item_or_default ("smtp", "localhost")) + l_site_name := utf.escaped_utf_32_string_to_utf_8_string_8 (a_cms_api.setup.site_name) + admin_email := a_cms_api.setup.site_email + + if not admin_email.has ('<') then + admin_email := l_site_name + " <" + admin_email +">" + end + + if attached {CONFIG_READER} a_cms_api.module_configuration ("login", Void) as cfg then + if attached cfg.text_item ("smtp") as l_smtp then + -- Overwrite global smtp setting if any. + smtp_server := utf.utf_32_string_to_utf_8_string_8 (l_smtp) + end + s := cfg.text_item ("email") + if s /= Void then + l_contact_email := utf.utf_32_string_to_utf_8_string_8 (s) + end + s := cfg.text_item ("subject_register") + if s /= Void then + l_subject_register := utf.utf_32_string_to_utf_8_string_8 (s) + end + s := cfg.text_item ("subject_activate") + if s /= Void then + l_subject_register := utf.utf_32_string_to_utf_8_string_8 (s) + end + s := cfg.text_item ("subject_password") + if s /= Void then + l_subject_register := utf.utf_32_string_to_utf_8_string_8 (s) + end + s := cfg.text_item ("subject_oauth") + if s /= Void then + l_subject_oauth := utf.utf_32_string_to_utf_8_string_8 (s) + end + + end + if l_contact_email /= Void then + if not l_contact_email.has ('<') then + l_contact_email := l_site_name + " <" + l_contact_email + ">" + end + contact_email := l_contact_email + else + contact_email := admin_email + end + if l_subject_register /= Void then + contact_subject_register := l_subject_register + else + contact_subject_register := "Thank you for registering with us." + end + + if l_subject_activate /= Void then + contact_subject_activate := l_subject_activate + else + contact_subject_activate := "New account activation token." + end + if l_subject_password /= Void then + contact_subject_password := l_subject_password + else + contact_subject_password := "Password Recovery." + end + if l_subject_oauth /= Void then + contact_subject_oauth := l_subject_oauth + else + contact_subject_oauth := "Welcome." + end + + end + +feature -- Access + + smtp_server: IMMUTABLE_STRING_8 + + admin_email: IMMUTABLE_STRING_8 + + contact_email: IMMUTABLE_STRING_8 + -- Contact email. + + contact_subject_register: IMMUTABLE_STRING_8 + contact_subject_activate: IMMUTABLE_STRING_8 + contact_subject_password: IMMUTABLE_STRING_8 + contact_subject_oauth: IMMUTABLE_STRING_8 + + + + account_activation: STRING + -- Account activation template email message. + local + p: PATH + do + p := setup.environment.config_path.extended ("modules").extended ("login").extended("account_activation.html") + if attached read_template_file (p) as l_content then + Result := l_content + else + create Result.make_from_string (template_account_activation) + end + end + + account_re_activation: STRING + -- Account re_activation template email message. + local + p: PATH + do + p := setup.environment.config_path.extended ("modules").extended ("login").extended("accunt_re_activation.html") + if attached read_template_file (p) as l_content then + Result := l_content + else + create Result.make_from_string (template_account_re_activation) + end + end + + account_password: STRING + -- Account password template email message. + local + p: PATH + do + p := setup.environment.config_path.extended ("modules").extended ("login").extended("account_new_password.html") + if attached read_template_file (p) as l_content then + Result := l_content + else + create Result.make_from_string (template_account_new_password) + end + end + + account_welcome: STRING + -- Account welcome template email message. + local + p: PATH + do + p := setup.environment.config_path.extended ("modules").extended ("login").extended("account_welcome.html") + if attached read_template_file (p) as l_content then + Result := l_content + else + create Result.make_from_string (template_account_welcome) + end + end + +feature {NONE} -- Implementation + + setup: CMS_SETUP + + + read_template_file (a_path: PATH): detachable STRING + -- Read the content of the file at path `a_path'. + local + l_file: FILE + do + create {PLAIN_TEXT_FILE} l_file.make_with_path (a_path) + if l_file.exists and then l_file.is_readable then + l_file.open_read + l_file.read_stream (l_file.count) + Result := l_file.last_string + l_file.close + else + -- Error + end + end + + +feature {NONE} -- Message email + + template_account_activation: STRING= "[ + + + + + Activation + + + + + +

Thank you for registering at ROC CMS

+ +

To complete your registration, please click on this link to activate your account:

+ +

$link

+

Thank you for joining us.

+ + + ]" + + + template_account_re_activation: STRING= "[ + + + + + New Activation + + + + + +

You have request a new activation token atROC CMS

+ +

To complete your registration, please click on this link to activate your account:

+ +

$link

+

Thank you for joining us.

+ + + ]" + + + + template_account_new_password: STRING= "[ + + + + + New Password + + + + + +

You have required a new password at ROC CMS

+ +

To complete your request, please click on this link to genereate a new password:

+ +

$link

+ + + ]" + + + template_account_welcome: STRING= "[ + + + + + Welcome + + + + + +

Welcome toROC CMS

+

Thank you for joining us.

+ + + ]" + +end diff --git a/modules/login/cms_authentication_module.e b/modules/login/cms_authentication_module.e new file mode 100644 index 0000000..58e1cdd --- /dev/null +++ b/modules/login/cms_authentication_module.e @@ -0,0 +1,890 @@ +note + description: "Module Logging supporting different authentication strategies" + date: "$Date: 2015-05-20 06:50:50 -0300 (mi. 20 de may. de 2015) $" + revision: "$Revision: 97328 $" + +class + CMS_AUTHENTICATION_MODULE + +inherit + CMS_MODULE + rename + module_api as user_oauth_api + redefine + filters, + register_hooks, + initialize, + is_installed, + install, + user_oauth_api + end + + + CMS_HOOK_BLOCK + + CMS_HOOK_AUTO_REGISTER + + CMS_HOOK_MENU_SYSTEM_ALTER + + CMS_HOOK_VALUE_TABLE_ALTER + + SHARED_EXECUTION_ENVIRONMENT + export + {NONE} all + end + + REFACTORING_HELPER + + SHARED_LOGGER + + CMS_REQUEST_UTIL + + +create + make + +feature {NONE} -- Initialization + + make + -- Create current module + do + name := "login" + version := "1.0" + description := "Eiffel login module" + package := "login" + + create root_dir.make_current + cache_duration := 0 + end + +feature {CMS_API} -- Module Initialization + + initialize (a_api: CMS_API) + -- + local + l_user_auth_api: like user_oauth_api + l_user_auth_storage: CMS_OAUTH_20_STORAGE_I + do + Precursor (a_api) + + -- Storage initialization + if attached {CMS_STORAGE_SQL_I} a_api.storage as l_storage_sql then + create {CMS_OAUTH_20_STORAGE_SQL} l_user_auth_storage.make (l_storage_sql) + else + -- FIXME: in case of NULL storage, should Current be disabled? + create {CMS_OAUTH_20_STORAGE_NULL} l_user_auth_storage + end + + -- Node API initialization + create l_user_auth_api.make_with_storage (a_api, l_user_auth_storage) + user_oauth_api := l_user_auth_api + ensure then + user_oauth_api_set: user_oauth_api /= Void + end + +feature {CMS_API} -- Module management + + is_installed (api: CMS_API): BOOLEAN + -- Is Current module installed? + do + Result := attached api.storage.custom_value ("is_initialized", "module-" + name) as v and then v.is_case_insensitive_equal_general ("yes") + end + + install (api: CMS_API) + local + l_setup: CMS_SETUP + l_params: detachable STRING_TABLE [detachable ANY] + l_consumers: LIST [STRING] + do + l_setup := api.setup + + -- Schema + if attached {CMS_STORAGE_SQL_I} api.storage as l_sql_storage then + if not l_sql_storage.sql_table_exists ("oauth2_consumers") then + --| Schema + l_sql_storage.sql_execute_file_script (l_setup.environment.path.extended ("scripts").extended ("oauth2_consumers.sql")) + + if l_sql_storage.has_error then + api.logger.put_error ("Could not initialize database for blog module", generating_type) + end + -- TODO workaround. + l_sql_storage.sql_execute_file_script (l_setup.environment.path.extended ("scripts").extended ("oauth2_consumers_initialize.sql")) + end + + -- TODO workaround, until we have an admin module + l_sql_storage.sql_query ("SELECT name FROM oauth2_consumers;", Void) + if l_sql_storage.has_error then + api.logger.put_error ("Could not initialize database for differnent consumerns", generating_type) + else + from + l_sql_storage.sql_start + create {ARRAYED_LIST[STRING]} l_consumers.make (2) + until + l_sql_storage.sql_after + loop + if attached l_sql_storage.sql_read_string (1) as l_name then + l_consumers.force ("oauth2_"+l_name) + end + l_sql_storage.sql_forth + end + across l_consumers as ic loop + if not l_sql_storage.sql_table_exists (ic.item) then + create l_params.make (1) + l_params.force (ic.item, "table_name") + l_sql_storage.sql_execute_file_script_with_params (l_setup.environment.path.extended ("scripts").extended ("oauth2_template.sql"), l_params) + end + end + end + api.storage.set_custom_value ("is_initialized", "module-" + name, "yes") + end + end + +feature {CMS_API} -- Access: API + + user_oauth_api: detachable CMS_OAUTH_20_API + -- + +feature -- Filters + + filters (a_api: CMS_API): detachable LIST [WSF_FILTER] + -- Possibly list of Filter's module. + do + create {ARRAYED_LIST [WSF_FILTER]} Result.make (1) + if attached user_oauth_api as l_user_oauth_api then + Result.extend (create {CMS_OAUTH_20_FILTER}.make (a_api, l_user_oauth_api)) + end + end + +feature -- Access: docs + + root_dir: PATH + + cache_duration: INTEGER + -- Caching duration + --| 0: disable + --| -1: cache always valie + --| nb: cache expires after nb seconds. + + cache_disabled: BOOLEAN + do + Result := cache_duration = 0 + end + +feature -- Router + + + setup_router (a_router: WSF_ROUTER; a_api: CMS_API) + -- + do + if attached user_oauth_api as l_user_oauth_api then + configure_web (a_api, l_user_oauth_api, a_router) + end + end + + + configure_web (a_api: CMS_API; a_user_oauth_api: CMS_OAUTH_20_API; a_router: WSF_ROUTER) + do + a_router.handle ("/account/roc-login", create {WSF_URI_AGENT_HANDLER}.make (agent handle_login (a_api, ?, ?)), a_router.methods_head_get) + a_router.handle ("/account/roc-register", create {WSF_URI_AGENT_HANDLER}.make (agent handle_register (a_api, ?, ?)), a_router.methods_get_post) + a_router.handle ("/account/activate/{token}", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent handle_activation (a_api, ?, ?)), a_router.methods_head_get) + a_router.handle ("/account/reactivate", create {WSF_URI_AGENT_HANDLER}.make (agent handle_reactivation (a_api, ?, ?)), a_router.methods_get_post) + a_router.handle ("/account/new-password", create {WSF_URI_AGENT_HANDLER}.make (agent handle_new_password (a_api, ?, ?)), a_router.methods_get_post) + a_router.handle ("/account/reset-password", create {WSF_URI_AGENT_HANDLER}.make (agent handle_reset_password (a_api, ?, ?)), a_router.methods_get_post) + a_router.handle ("/account/roc-logout", create {WSF_URI_AGENT_HANDLER}.make (agent handle_logout (a_api, ?, ?)), a_router.methods_get_post) + a_router.handle ("/account/login-with-oauth/{callback}", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent handle_login_with_oauth (a_api,a_user_oauth_api, ?, ?)), a_router.methods_get_post) + a_router.handle ("/account/{callback}", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent handle_callback_oauth (a_api, a_user_oauth_api, ?, ?)), a_router.methods_get_post) + end + + +feature -- Hooks configuration + + register_hooks (a_response: CMS_RESPONSE) + -- Module hooks configuration. + do + auto_subscribe_to_hooks (a_response) + a_response.subscribe_to_block_hook (Current) + a_response.subscribe_to_value_table_alter_hook (Current) + end + +feature -- Hooks + + value_table_alter (a_value: CMS_VALUE_TABLE; a_response: CMS_RESPONSE) + -- + do + if attached current_user (a_response.request) as l_user then + a_value.force (l_user, "user") + end + end + + menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE) + -- Hook execution on collection of menu contained by `a_menu_system' + -- for related response `a_response'. + local + lnk: CMS_LOCAL_LINK + do + if attached a_response.current_user (a_response.request) as u then + create lnk.make (u.name + " (Logout)", "account/roc-logout" ) + else + create lnk.make ("Login", "account/roc-login") + end + a_menu_system.primary_menu.extend (lnk) + lnk.set_weight (98) + end + + block_list: ITERABLE [like {CMS_BLOCK}.name] + local + l_string: STRING + do + Result := <<"login","register","reactivate","new_password", "reset_password">> + create l_string.make_empty + across Result as ic loop + l_string.append (ic.item) + l_string.append_character (' ') + end + write_debug_log (generator + ".block_list:" + l_string ) + end + + get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE) + do + if + a_block_id.is_case_insensitive_equal_general ("login") and then + a_response.request.path_info.starts_with ("/account/roc-login") + then + get_block_view_login (a_block_id, a_response) + elseif + a_block_id.is_case_insensitive_equal_general ("register") and then + a_response.request.path_info.starts_with ("/account/roc-register") + then + get_block_view_register (a_block_id, a_response) + elseif + a_block_id.is_case_insensitive_equal_general ("reactivate") and then + a_response.request.path_info.starts_with ("/account/reactivate") + then + get_block_view_reactivate (a_block_id, a_response) + elseif + a_block_id.is_case_insensitive_equal_general ("new_password") and then + a_response.request.path_info.starts_with ("/account/new-password") + then + get_block_view_new_password (a_block_id, a_response) + elseif + a_block_id.is_case_insensitive_equal_general ("reset_password") and then + a_response.request.path_info.starts_with ("/account/reset-password") + then + get_block_view_reset_password (a_block_id, a_response) + end + end + + handle_login (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE) + local + r: CMS_RESPONSE + do + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + r.set_value ("Login", "optional_content_type") + r.execute + end + + handle_logout (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE) + local + r: CMS_RESPONSE + l_url: STRING + l_cookie: WSF_COOKIE + do + if + attached {WSF_STRING} req.cookie ({CMS_AUTHENTICATION_CONSTANTS}.oauth_session) as l_cookie_token and then + attached {CMS_USER} current_user (req) as l_user + then + -- Logout gmail + create l_cookie.make ({CMS_AUTHENTICATION_CONSTANTS}.oauth_session, l_cookie_token.value) + l_cookie.set_path ("/") + l_cookie.set_max_age (-1) + res.add_cookie (l_cookie) + unset_current_user (req) + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + r.set_status_code ({HTTP_CONSTANTS}.found) + r.set_redirection (req.absolute_script_url ("")) + r.execute + else + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + r.set_status_code ({HTTP_CONSTANTS}.found) + l_url := req.absolute_script_url ("") + l_url.append ("/basic_auth_logoff") + r.set_redirection (l_url) + r.execute + end + end + + handle_register (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE) + local + r: CMS_RESPONSE + l_user_api: CMS_USER_API + u: CMS_USER + l_roles: LIST [CMS_USER_ROLE] + l_exist: BOOLEAN + es: CMS_AUTHENTICATON_EMAIL_SERVICE + l_link: STRING + l_token: STRING + do + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + r.set_value ("Register", "optional_content_type") + if req.is_post_request_method then + if + attached {WSF_STRING} req.form_parameter ("name") as l_name and then + attached {WSF_STRING} req.form_parameter ("password") as l_password and then + attached {WSF_STRING} req.form_parameter ("email") as l_email + then + l_user_api := api.user_api + + if attached l_user_api.user_by_name (l_name.value) then + -- Username already exist. + r.values.force ("The user name exist!", "error_name") + l_exist := True + end + if attached l_user_api.user_by_email (l_email.value) then + -- Emails already exist. + r.values.force ("The email exist!", "error_email") + l_exist := True + end + + if not l_exist then + -- New user + create {ARRAYED_LIST [CMS_USER_ROLE]}l_roles.make (1) + l_roles.force (l_user_api.authenticated_user_role) + + create u.make (l_name.value) + u.set_email (l_email.value) + u.set_password (l_password.value) + u.set_roles (l_roles) + l_user_api.new_user (u) + + -- Create activation token + l_token := new_token + l_user_api.new_activation (l_token, u.id) + create l_link.make_from_string (req.server_url) + l_link.append ("/account/activate/") + l_link.append (l_token) + + + -- Send Email + create es.make (create {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}.make (api)) + write_debug_log (generator + ".handle register: send_contact_email") + es.send_contact_email (l_email.value, l_link) + + else + r.values.force (l_name.value, "name") + r.values.force (l_email.value, "email") + r.set_status_code ({HTTP_CONSTANTS}.bad_request) + end + end + end + + r.execute + end + + handle_activation (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE) + local + r: CMS_RESPONSE + l_user_api: CMS_USER_API + l_ir: INTERNAL_SERVER_ERROR_CMS_RESPONSE + do + l_user_api := api.user_api + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + if attached {WSF_STRING} req.path_parameter ("token") as l_token then + + if attached {CMS_USER} l_user_api.user_by_activation_token (l_token.value) as l_user then + -- Valid user_id + l_user.mark_active + l_user_api.update_user (l_user) + l_user_api.remove_activation (l_token.value) + r.set_value ("Account activated", "optional_content_type") + r.set_main_content ("

Your account "+ l_user.name +" has been activated

") + else + -- the token does not exist, or it was already used. + r.set_status_code ({HTTP_CONSTANTS}.bad_request) + r.set_value ("Account not activated", "optional_content_type") + r.set_main_content ("

The token "+ l_token.value +" is not valid Reactivate Account

" ) + + end + r.execute + else + create l_ir.make (req, res, api) + l_ir.execute + end + end + + + handle_reactivation (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE) + local + r: CMS_RESPONSE + es: CMS_AUTHENTICATON_EMAIL_SERVICE + l_user_api: CMS_USER_API + l_token: STRING + l_link: STRING + do + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + if req.is_post_request_method then + if + attached {WSF_STRING} req.form_parameter ("email") as l_email + then + l_user_api := api.user_api + if attached {CMS_USER} l_user_api.user_by_email (l_email.value) as l_user then + -- User exist create a new token and send a new email. + if l_user.is_active then + r.values.force ("The asociated user to the given email " + l_email.value + " , is already active", "is_active") + r.set_status_code ({HTTP_CONSTANTS}.bad_request) + else + l_token := new_token + l_user_api.new_activation (l_token, l_user.id) + create l_link.make_from_string (req.server_url) + l_link.append ("/account/activate/") + l_link.append (l_token) + + -- Send Email + create es.make (create {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}.make (api)) + write_debug_log (generator + ".handle register: send_contact_activation_email") + es.send_contact_activation_email (l_email.value, l_link) + end + else + r.values.force ("The email does not exist or !", "error_email") + r.values.force (l_email.value, "email") + r.set_status_code ({HTTP_CONSTANTS}.bad_request) + end + end + end + + r.execute + end + + handle_new_password (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE) + local + r: CMS_RESPONSE + es: CMS_AUTHENTICATON_EMAIL_SERVICE + l_user_api: CMS_USER_API + l_token: STRING + l_link: STRING + do + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + if req.is_post_request_method then + l_user_api := api.user_api + if attached {WSF_STRING} req.form_parameter ("email") as l_email then + if attached {CMS_USER} l_user_api.user_by_email (l_email.value) as l_user then + -- User exist create a new token and send a new email. + l_token := new_token + l_user_api.new_password (l_token, l_user.id) + create l_link.make_from_string (req.server_url) + l_link.append ("/account/reset-password?token=") + l_link.append (l_token) + + -- Send Email + create es.make (create {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}.make (api)) + write_debug_log (generator + ".handle register: send_contact_password_email") + es.send_contact_password_email (l_email.value, l_link) + else + r.values.force ("The email does not exist !", "error_email") + r.values.force (l_email.value, "email") + r.set_status_code ({HTTP_CONSTANTS}.bad_request) + end + end + end + r.execute + end + + + handle_reset_password (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE) + local + r: CMS_RESPONSE + l_user_api: CMS_USER_API + do + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + l_user_api := api.user_api + if attached {WSF_STRING} req.query_parameter ("token") as l_token then + r.values.force (l_token.value, "token") + if l_user_api.user_by_password_token (l_token.value) = Void then + r.values.force ("The token " + l_token.value + " is not valid, click here to generate a new token.", "error_token") + r.set_status_code ({HTTP_CONSTANTS}.bad_request) + end + end + + if req.is_post_request_method then + + if + attached {WSF_STRING} req.form_parameter ("token") as l_token and then + attached {WSF_STRING} req.form_parameter ("password") as l_password and then + attached {WSF_STRING} req.form_parameter ("confirm_password") as l_confirm_password + then + -- Does the passwords match? + if l_password.value.same_string (l_confirm_password.value) then + -- is the token valid? + if attached {CMS_USER} l_user_api.user_by_password_token (l_token.value) as l_user then + l_user.set_password (l_password.value) + l_user_api.update_user (l_user) + l_user_api.remove_password (l_token.value) + end + else + r.values.force ("Passwords Don't Match", "error_password") + r.values.force (l_token.value, "token") + r.set_status_code ({HTTP_CONSTANTS}.bad_request) + end + end + end + r.execute + end + +feature {NONE} -- Helpers + + template_block (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE): detachable CMS_SMARTY_TEMPLATE_BLOCK + -- Smarty content block for `a_block_id' + local + p: detachable PATH + do + create p.make_from_string ("templates") + p := p.extended ("block_").appended (a_block_id).appended_with_extension ("tpl") + p := a_response.module_resource_path (Current, p) + if p /= Void then + if attached p.entry as e then + create Result.make (a_block_id, Void, p.parent, e) + else + create Result.make (a_block_id, Void, p.parent, p) + end + end + end + +feature {NONE} -- Block views + + get_block_view_login (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE) + local + vals: CMS_VALUE_TABLE + do + if attached template_block (a_block_id, a_response) as l_tpl_block then + create vals.make (1) + -- add the variable to the block + value_table_alter (vals, a_response) + across + vals as ic + loop + l_tpl_block.set_value (ic.item, ic.key) + end + if + attached user_oauth_api as l_auth_api and then + attached l_auth_api.oauth2_consumers as l_list + then + l_tpl_block.set_value (l_list, "oauth_consumers") + end + + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + end + + get_block_view_register (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE) + do + if a_response.request.is_get_request_method then + if attached template_block (a_block_id, a_response) as l_tpl_block then + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + elseif a_response.request.is_post_request_method then + if a_response.values.has ("error_name") or else a_response.values.has ("error_email") then + if attached template_block (a_block_id, a_response) as l_tpl_block then + l_tpl_block.set_value (a_response.values.item ("error_name"), "error_name") + l_tpl_block.set_value (a_response.values.item ("error_email"), "error_email") + l_tpl_block.set_value (a_response.values.item ("email"), "email") + l_tpl_block.set_value (a_response.values.item ("name"), "name") + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + else + if attached template_block ("post_register", a_response) as l_tpl_block then + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + end + end + end + + + get_block_view_reactivate (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE) + do + if a_response.request.is_get_request_method then + if attached template_block (a_block_id, a_response) as l_tpl_block then + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + elseif a_response.request.is_post_request_method then + if a_response.values.has ("error_email") or else a_response.values.has ("is_active") then + if attached template_block (a_block_id, a_response) as l_tpl_block then + l_tpl_block.set_value (a_response.values.item ("error_email"), "error_email") + l_tpl_block.set_value (a_response.values.item ("email"), "email") + l_tpl_block.set_value (a_response.values.item ("is_active"), "is_active") + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + else + if attached template_block ("post_reactivate", a_response) as l_tpl_block then + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + end + end + end + + get_block_view_new_password (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE) + do + if a_response.request.is_get_request_method then + if attached template_block (a_block_id, a_response) as l_tpl_block then + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + elseif a_response.request.is_post_request_method then + if a_response.values.has ("error_email") then + if attached template_block (a_block_id, a_response) as l_tpl_block then + l_tpl_block.set_value (a_response.values.item ("error_email"), "error_email") + l_tpl_block.set_value (a_response.values.item ("email"), "email") + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + else + if attached template_block ("post_password", a_response) as l_tpl_block then + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + end + end + end + + get_block_view_reset_password (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE) + do + if a_response.request.is_get_request_method then + if attached template_block (a_block_id, a_response) as l_tpl_block then + l_tpl_block.set_value (a_response.values.item ("token"), "token") + l_tpl_block.set_value (a_response.values.item ("error_token"), "error_token") + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + elseif a_response.request.is_post_request_method then + if a_response.values.has ("error_token") or else a_response.values.has ("error_password") then + if attached template_block (a_block_id, a_response) as l_tpl_block then + l_tpl_block.set_value (a_response.values.item ("error_token"), "error_token") + l_tpl_block.set_value (a_response.values.item ("error_password"), "error_password") + l_tpl_block.set_value (a_response.values.item ("token"), "token") + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + else + if attached template_block ("post_reset", a_response) as l_tpl_block then + a_response.add_block (l_tpl_block, "content") + else + debug ("cms") + a_response.add_warning_message ("Error with block [" + a_block_id + "]") + end + end + end + end + end + +feature -- OAuth2 Login with google. + + handle_login_with_oauth (api: CMS_API; a_oauth_api: CMS_OAUTH_20_API; req: WSF_REQUEST; res: WSF_RESPONSE) + local + r: CMS_RESPONSE + l_oauth: CMS_OAUTH_20_WORKFLOW + do + if + attached {WSF_STRING} req.path_parameter ("callback") as p_consumer and then + attached {CMS_OAUTH_20_CONSUMER} a_oauth_api.oauth_consumer_by_name (p_consumer.value) as l_consumer + then + create l_oauth.make (req.server_url, l_consumer) + if attached l_oauth.authorization_url as l_authorization_url then + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + r.set_redirection (l_authorization_url) + r.execute + else + create {BAD_REQUEST_ERROR_CMS_RESPONSE} r.make (req, res, api) + r.set_main_content ("Bad request") + r.execute + end + else + create {BAD_REQUEST_ERROR_CMS_RESPONSE} r.make (req, res, api) + r.set_main_content ("Bad request") + r.execute + end + end + + handle_callback_oauth (api: CMS_API; a_user_oauth_api: CMS_OAUTH_20_API; req: WSF_REQUEST; res: WSF_RESPONSE) + local + r: CMS_RESPONSE + l_auth: CMS_OAUTH_20_WORKFLOW + l_user_api: CMS_USER_API + l_user: CMS_USER + l_roles: LIST [CMS_USER_ROLE] + l_cookie: WSF_COOKIE + es: CMS_AUTHENTICATON_EMAIL_SERVICE + do + if attached {WSF_STRING} req.path_parameter ("callback") as l_callback and then + attached {CMS_OAUTH_20_CONSUMER} a_user_oauth_api.oauth_consumer_by_callback (l_callback.value) as l_consumer and then + attached {WSF_STRING} req.query_parameter ("code") as l_code + then + create l_auth.make (req.server_url, l_consumer) + l_auth.sign_request (l_code.value) + if + attached l_auth.access_token as l_access_token and then + attached l_auth.user_profile as l_user_profile + then + create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) + -- extract user email + -- check if the user exist + l_user_api := api.user_api + -- 1 if the user exit put it in the context + if + attached l_auth.user_email as l_email + then + if attached {CMS_USER} l_user_api.user_by_email (l_email) as p_user then + -- User with email exist + if attached {CMS_USER} a_user_oauth_api.user_oauth2_by_id (p_user.id, l_consumer.name) then + -- Update oauth entry + a_user_oauth_api.update_user_oauth2 (l_access_token.token, l_user_profile, p_user, l_consumer.name ) + else + -- create a oauth entry + a_user_oauth_api.new_user_oauth2 (l_access_token.token, l_user_profile, p_user, l_consumer.name ) + end + create l_cookie.make ({CMS_AUTHENTICATION_CONSTANTS}.oauth_session, l_access_token.token) + l_cookie.set_max_age (l_access_token.expires_in) + l_cookie.set_path ("/") + res.add_cookie (l_cookie) + else + + create {ARRAYED_LIST [CMS_USER_ROLE]}l_roles.make (1) + l_roles.force (l_user_api.authenticated_user_role) + + -- Create a new user and oauth entry + create l_user.make (l_email) + l_user.set_email (l_email) + l_user.set_password (new_token) -- generate a random password. + l_user.set_roles (l_roles) + l_user.mark_active + l_user_api.new_user (l_user) + + -- Add oauth entry + a_user_oauth_api.new_user_oauth2 (l_access_token.token, l_user_profile, l_user, l_consumer.name ) + create l_cookie.make ({CMS_AUTHENTICATION_CONSTANTS}.oauth_session, l_access_token.token) + l_cookie.set_max_age (l_access_token.expires_in) + l_cookie.set_path ("/") + res.add_cookie (l_cookie) + set_current_user (req, l_user) + + + -- Send Email + create es.make (create {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}.make (api)) + write_debug_log (generator + ".handle register: send_contact_welcome_email") + es.send_contact_welcome_email (l_email, "") + end + else + end + r.set_redirection (req.absolute_script_url ("")) + r.execute + end + + end + + end + +feature {NONE} -- Token Generation + + new_token: STRING + -- Generate a new token activation token + local + l_token: STRING + l_security: SECURITY_PROVIDER + l_encode: URL_ENCODER + do + create l_security + l_token := l_security.token + create l_encode + from until l_token.same_string (l_encode.encoded_string (l_token)) loop + -- Loop ensure that we have a security token that does not contain characters that need encoding. + -- We cannot simply to an encode-decode because the email sent to the user will contain an encoded token + -- but the user will need to use an unencoded token if activation has to be done manually. + l_token := l_security.token + end + Result := l_token + end + + + +feature {NONE} -- Implementation: date and time + + http_date_format_to_date (s: READABLE_STRING_8): detachable DATE_TIME + local + d: HTTP_DATE + do + create d.make_from_string (s) + if not d.has_error then + Result := d.date_time + end + end + + file_date (p: PATH): DATE_TIME + require + path_exists: (create {FILE_UTILITIES}).file_path_exists (p) + local + f: RAW_FILE + do + create f.make_with_path (p) + Result := timestamp_to_date (f.date) + end + + timestamp_to_date (n: INTEGER): DATE_TIME + local + d: HTTP_DATE + do + create d.make_from_timestamp (n) + Result := d.date_time + end + + +note + copyright: "Copyright (c) 1984-2013, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" +end diff --git a/modules/login/cms_authenticaton_email_service.e b/modules/login/cms_authenticaton_email_service.e new file mode 100644 index 0000000..d4a1984 --- /dev/null +++ b/modules/login/cms_authenticaton_email_service.e @@ -0,0 +1,88 @@ +note + description: "Summary description for {CMS_AUTHENTICATON_EMAIL_SERVICE}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_AUTHENTICATON_EMAIL_SERVICE + +inherit + EMAIL_SERVICE + redefine + initialize, + parameters + end + +create + make + +feature {NONE} -- Initialization + + initialize + do + Precursor + contact_email := parameters.contact_email + end + + parameters: CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS + -- Associated parameters. + +feature -- Access + + contact_email: IMMUTABLE_STRING_8 + -- contact email. + +feature -- Basic Operations + + send_contact_email (a_to, a_content: READABLE_STRING_8) + -- Send successful contact message `a_token' to `a_to'. + require + attached_to: a_to /= Void + local + l_message: STRING + do + create l_message.make_from_string (parameters.account_activation) + l_message.replace_substring_all ("$link", a_content) + send_message (contact_email, a_to, parameters.contact_subject_register, l_message) + end + + + send_contact_activation_email (a_to, a_content: READABLE_STRING_8) + -- Send successful contact message `a_token' to `a_to'. + require + attached_to: a_to /= Void + local + l_message: STRING + do + create l_message.make_from_string (parameters.account_re_activation) + l_message.replace_substring_all ("$link", a_content) + send_message (contact_email, a_to, parameters.contact_subject_activate, l_message) + end + + + send_contact_password_email (a_to, a_content: READABLE_STRING_8) + -- Send successful contact message `a_token' to `a_to'. + require + attached_to: a_to /= Void + local + l_message: STRING + do + create l_message.make_from_string (parameters.account_password) + l_message.replace_substring_all ("$link", a_content) + send_message (contact_email, a_to, parameters.contact_subject_password, l_message) + end + + send_contact_welcome_email (a_to, a_content: READABLE_STRING_8) + -- Send successful contact message `a_token' to `a_to'. + require + attached_to: a_to /= Void + local + l_message: STRING + do + create l_message.make_from_string (parameters.account_welcome) + l_message.replace_substring_all ("$link", a_content) + send_message (contact_email, a_to, parameters.contact_subject_oauth, l_message) + end + + +end diff --git a/modules/login/cms_oauth_20_api.e b/modules/login/cms_oauth_20_api.e new file mode 100644 index 0000000..27739b2 --- /dev/null +++ b/modules/login/cms_oauth_20_api.e @@ -0,0 +1,95 @@ +note + description: "[ + API to manage CMS User OAuth authentication. + ]" + date: "$Date$" + revision: "$Revision$" + +class + CMS_OAUTH_20_API + +inherit + CMS_MODULE_API + + REFACTORING_HELPER + +create {CMS_AUTHENTICATION_MODULE} + make_with_storage + +feature {NONE} -- Initialization + + make_with_storage (a_api: CMS_API; a_oauth_storage: CMS_OAUTH_20_STORAGE_I) + -- Create an object with api `a_api' and storage `a_oauth_storage'. + do + oauth_20_storage := a_oauth_storage + make (a_api) + ensure + oauht_20_storage_set: oauth_20_storage = a_oauth_storage + end + +feature {CMS_MODULE} -- Access: User oauth storage. + + oauth_20_storage: CMS_OAUTH_20_STORAGE_I + -- storage interface. + +feature -- Access: User Oauth20 + + user_oauth2_by_id (a_uid: like {CMS_USER}.id; a_consumer: READABLE_STRING_32): detachable CMS_USER + -- Retrieve a user by id `a_uid' for the consumer `a_consumer', if aby. + do + Result := oauth_20_storage.user_oauth2_by_id (a_uid, a_consumer) + end + + user_oauth2_by_token (a_token: READABLE_STRING_32; a_consumer: READABLE_STRING_32): detachable CMS_USER + -- Retrieve a user by token `a_token' for the consumer `a_consumer'. + do + Result := oauth_20_storage.user_oauth2_by_token (a_token, a_consumer) + end + + user_oauth2_without_consumer_by_token (a_token: READABLE_STRING_32 ): detachable CMS_USER + -- Retrieve a user by token `a_token' searching in all the registered consumers in the system. + do + Result := oauth_20_storage.user_oauth2_without_consumer_by_token (a_token) + end + +feature -- Access: Consumers OAuth20 + + oauth2_consumers: LIST [STRING] + -- List of Oauth_20 consumers, if any, empty in other case. + do + Result := oauth_20_storage.oauth2_consumers + end + + oauth_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER + -- Retrieve a consumer by name `a_name', if any. + do + Result := oauth_20_storage.oauth_consumer_by_name (a_name) + end + + oauth_consumer_by_callback (a_callback: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER + -- Retrieve a consumer by callback `a_callback', if any. + do + Result := oauth_20_storage.oauth_consumer_by_callback (a_callback) + end + +feature -- Change: User OAuth20 + + + new_user_oauth2 (a_token: READABLE_STRING_32; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer: READABLE_STRING_32) + -- Add a new user with oauth20 using the consumer `a_consumer'. + require + has_id: a_user.has_id + do + oauth_20_storage.new_user_oauth2 (a_token, a_user_profile, a_user, a_consumer) + end + + + update_user_oauth2 (a_token: READABLE_STRING_32; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer_table: READABLE_STRING_32) + -- Updaate user `a_user' with oauth2 for the consumer `a_consumer'. + require + has_id: a_user.has_id + do + oauth_20_storage.update_user_oauth2 (a_token, a_user_profile, a_user, a_consumer_table) + end + +end diff --git a/modules/login/cms_oauth_20_consumer.e b/modules/login/cms_oauth_20_consumer.e new file mode 100644 index 0000000..79f3421 --- /dev/null +++ b/modules/login/cms_oauth_20_consumer.e @@ -0,0 +1,152 @@ +note + description: "Summary description for {CMS_OAUTH_CONSUMER}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + CMS_OAUTH_20_CONSUMER + +inherit + + ANY + redefine + default_create + end + +create + default_create + +feature {NONE} -- Initialization + + default_create + do + set_endpoint ("") + set_authorize_url ("") + set_extractor ("") + set_callback_name ("") + set_protected_resource_url ("") + set_scope ("") + set_api_key ("") + set_api_secret ("") + set_name ("") + end + +feature -- Access + + endpoint: READABLE_STRING_32 + -- Url that receives the access token request. + + authorize_url: READABLE_STRING_32 + -- + + extractor: READABLE_STRING_32 + -- text, json + + + callback_name: READABLE_STRING_32 + -- consumer callback name + + protected_resource_url: READABLE_STRING_32 + -- consumer resource url + + scope: READABLE_STRING_32 + -- consumer scope + + api_key: READABLE_STRING_32 + -- consumer public key + + api_secret: READABLE_STRING_32 + -- consumer secret. + + name: READABLE_STRING_32 + -- consumer name. + + id: INTEGER_64 + -- unique identifier. + + + +feature -- Element change + + set_extractor (a_extractor: like extractor) + -- Assign `extractor' with `a_extractor'. + do + extractor := a_extractor + ensure + extractor_assigned: extractor = a_extractor + end + + set_authorize_url (a_authorize_url: like authorize_url) + -- Assign `authorize_url' with `a_authorize_url'. + do + authorize_url := a_authorize_url + ensure + authorize_url_assigned: authorize_url = a_authorize_url + end + + set_endpoint (a_endpoint: like endpoint) + -- Assign `endpoint' with `a_endpoint'. + do + endpoint := a_endpoint + ensure + endpoint_assigned: endpoint = a_endpoint + end + + set_callback_name (a_callback_name: like callback_name) + -- Assign `callback_name' with `a_callback_name'. + do + callback_name := a_callback_name + ensure + callback_name_assigned: callback_name = a_callback_name + end + + set_protected_resource_url (a_protected_resource_url: like protected_resource_url) + -- Assign `protected_resource_url' with `a_protected_resource_url'. + do + protected_resource_url := a_protected_resource_url + ensure + protected_resource_url_assigned: protected_resource_url = a_protected_resource_url + end + + set_scope (a_scope: like scope) + -- Assign `scope' with `a_scope'. + do + scope := a_scope + ensure + scope_assigned: scope = a_scope + end + + set_api_key (an_api_key: like api_key) + -- Assign `api_key' with `an_api_key'. + do + api_key := an_api_key + ensure + api_key_assigned: api_key = an_api_key + end + + set_api_secret (an_api_secret: like api_secret) + -- Assign `api_secret' with `an_api_secret'. + do + api_secret := an_api_secret + ensure + api_secret_assigned: api_secret = an_api_secret + end + + set_name (a_name: like name) + -- Assign `name' with `a_name'. + do + name := a_name + ensure + name_assigned: name = a_name + end + + set_id (an_id: like id) + -- Assign `id' with `an_id'. + do + id := an_id + ensure + id_assigned: id = an_id + end + +end diff --git a/modules/login/cms_oauth_20_workflow.e b/modules/login/cms_oauth_20_workflow.e new file mode 100644 index 0000000..876aea4 --- /dev/null +++ b/modules/login/cms_oauth_20_workflow.e @@ -0,0 +1,133 @@ +note + description: "OAuth workflow" + date: "$Date$" + revision: "$Revision$" + +class + CMS_OAUTH_20_WORKFLOW + +inherit + + SHARED_LOGGER + +create + make + +feature {NONE} -- Initialization + + make (a_host: READABLE_STRING_32; a_consumer: CMS_OAUTH_20_CONSUMER) + -- Create an object with the host `a_host'. + do + initilize (a_consumer) + create config.make_default (a_consumer.api_key, a_consumer.api_secret) + config.set_callback (a_host + "/account/"+ a_consumer.callback_name) + config.set_scope (a_consumer.scope) + --Todo create a generic OAUTH_20_GENERIC_API + create oauth_api.make (a_consumer.endpoint, a_consumer.authorize_url, a_consumer.extractor) + api_service := oauth_api.create_service (config) + end + + initilize (a_consumer: CMS_OAUTH_20_CONSUMER) + do + --Use configuration values if any if not defaul + api_key := a_consumer.api_key + api_secret := a_consumer.api_secret + scope := a_consumer.scope + protected_resource_url := a_consumer.protected_resource_url + end + +feature -- Access + + authorization_url: detachable READABLE_STRING_32 + -- Obtain the Authorization URL. + do + -- Obtain the Authorization URL + write_debug_log (generator + ".authorization_url Fetching the Authorization URL..!") + if attached api_service.authorization_url (empty_token) as l_authorization_url then + write_debug_log (generator + ".authorization_url: Got the Authorization URL!") + write_debug_log (generator + ".authorization_url:" + l_authorization_url) + Result := l_authorization_url.as_string_32 + end + end + + sign_request (a_code: READABLE_STRING_32) + -- Sign request with code `a_code'. + --! To get the code `a_code' you need to do a request + --! using the authorization_url + local + request: OAUTH_REQUEST + do + -- Get the access token. + write_debug_log (generator + ".sign_request Fetching the access token with code [" + a_code + "]") + access_token := api_service.access_token_post (empty_token, create {OAUTH_VERIFIER}.make (a_code)) + if attached access_token as l_access_token then + write_debug_log (generator + ".sign_request Got the Access Token [" + l_access_token.debug_output + "]") + -- Get the user email + --! at the moment the scope is mail, but we can change it to get more information. + create request.make ("GET", protected_resource_url) + request.add_header ("Authorization", "Bearer " + l_access_token.token) + api_service.sign_request (l_access_token, request) + if attached {OAUTH_RESPONSE} request.execute as l_response then + write_debug_log (generator + ".sign_request Sign_request response [" + l_response.status.out + "]") + if attached l_response.body as l_body then + user_profile := l_body + write_debug_log (generator + ".sign_request User profile [" + l_body + "]") + end + end + end + end + + user_email: detachable READABLE_STRING_32 + -- Retrieve user email if any. + local + l_json: JSON_CONFIG + do + if attached user_profile as l_profile then + create l_json.make_from_string (l_profile) + if + attached {JSON_ARRAY} l_json.item ("emails") as l_array and then + attached {JSON_OBJECT} l_array.i_th (1) as l_object and then + attached {JSON_STRING} l_object.item ("value") as l_email + then + Result := l_email.item + elseif attached {JSON_STRING} l_json.item ("email") as l_email then + Result := l_email.unescaped_string_32 + end + end + end + +feature -- Access + + access_token: detachable OAUTH_TOKEN + -- JSON representing the access token. + + user_profile: detachable READABLE_STRING_32 + -- JSON representing the user profiles. + +feature {NONE} -- Implementation + + oauth_api: CMS_OAUTH_20_GENERIC_API + -- OAuth 2.0 Google API. + + config: OAUTH_CONFIG + -- configuration. + + api_service: OAUTH_SERVICE_I + -- Service. + + api_key: STRING + -- public key. + + api_secret: STRING + -- secret key. + + scope: STRING + -- api scope to access protected resources. + + protected_resource_url: STRING + -- Resource url. + + empty_token: detachable OAUTH_TOKEN + -- fake token. + +end diff --git a/modules/login/filter/cms_oauth_20_filter.e b/modules/login/filter/cms_oauth_20_filter.e new file mode 100644 index 0000000..ef250ee --- /dev/null +++ b/modules/login/filter/cms_oauth_20_filter.e @@ -0,0 +1,58 @@ +note + description: "Summary description for {CMS_OAUTH_20_FILTER}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_OAUTH_20_FILTER + +inherit + WSF_URI_TEMPLATE_HANDLER + CMS_HANDLER + rename + make as make_handler + end + + WSF_FILTER + +create + make + +feature {NONE} -- Initialization + + make (a_api: CMS_API; a_user_oauth_api: CMS_OAUTH_20_API) + do + make_handler (a_api) + user_oauth_api := a_user_oauth_api + end + + user_oauth_api: CMS_OAUTH_20_API + +feature -- Basic operations + + execute (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Execute the filter. + local + do + api.logger.put_debug (generator + ".execute ", Void) +-- if attached req.raw_header_data as l_raw_data then +-- api.logger.put_debug (generator + ".execute " + utf.escaped_utf_32_string_to_utf_8_string_8 (l_raw_data), Void) +-- end + -- A valid user + if + attached {WSF_STRING} req.cookie ({CMS_AUTHENTICATION_CONSTANTS}.oauth_session) as l_roc_auth_session_token + then + if attached {CMS_USER} user_oauth_api.user_oauth2_without_consumer_by_token (l_roc_auth_session_token.value) as l_user then + set_current_user (req, l_user) + execute_next (req, res) + else + api.logger.put_error (generator + ".execute login_valid failed for: " + l_roc_auth_session_token.value , Void) + execute_next (req, res) + end + else + api.logger.put_debug (generator + ".execute without authentication", Void) + execute_next (req, res) + end + end + +end diff --git a/modules/login/login-safe.ecf b/modules/login/login-safe.ecf new file mode 100644 index 0000000..36d6bff --- /dev/null +++ b/modules/login/login-safe.ecf @@ -0,0 +1,30 @@ + + + + + + /.git$ + /EIFGENs$ + /.svn$ + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/login/persistence/cms_oauth_20_generic_api.e b/modules/login/persistence/cms_oauth_20_generic_api.e new file mode 100644 index 0000000..552d2ea --- /dev/null +++ b/modules/login/persistence/cms_oauth_20_generic_api.e @@ -0,0 +1,94 @@ +note + description: "Generic OAUTH2 API" + date: "$Date$" + revision: "$Revision$" + +class + CMS_OAUTH_20_GENERIC_API + +inherit + + OAUTH_20_API + redefine + access_token_extractor, + access_token_verb + end + +create + make + +feature {NONE} -- Initialize + + make (a_endpoint: READABLE_STRING_8; a_authorize_url: READABLE_STRING_8; a_extractor: READABLE_STRING_8) + do + endpoint := a_endpoint + authorize_url := a_authorize_url + extractor := a_extractor + ensure + endpoint_set: endpoint = a_endpoint + authorize_url_set: authorize_url = a_authorize_url + extractor_set: extractor = a_authorize_url + end + + endpoint: READABLE_STRING_8 + -- Url that receives the access token request. + + authorize_url: READABLE_STRING_8 + -- + + extractor: READABLE_STRING_8 + -- text, json + +feature -- Access + + access_token_extractor: ACCESS_TOKEN_EXTRACTOR + -- Return token extractor, by default TOKEN_EXTRACTOR_20. + do + if extractor.is_case_insensitive_equal_general ("json") then + create {JSON_TOKEN_EXTRACTOR} Result + else + create {TOKEN_EXTRACTOR_20} Result + end + end + + access_token_verb: STRING_8 + do + Result := "POST" + end + + access_token_endpoint: STRING_8 + -- Url that receives the access token request + do + create Result.make_from_string (endpoint) + end + + authorization_url (config: OAUTH_CONFIG): detachable STRING_8 + -- Url where you should redirect your users to authneticate + local + l_result: STRING_8 + do + if attached config.scope as l_scope then + create l_result.make_from_string (authorize_url + SCOPED_AUTHORIZE_URL) + l_result.replace_substring_all ("$CLIENT_ID", config.api_key.as_string_8) + if attached config.callback as l_callback then + l_result.replace_substring_all ("$REDIRECT_URI", (create {OAUTH_ENCODER}).encoded_string (l_callback.as_string_8)) + end + if attached config.callback as l_callback then + l_result.replace_substring_all ("$SCOPE", (create {OAUTH_ENCODER}).encoded_string (l_scope.as_STRING_8)) + Result := l_result + end + else + create l_result.make_from_string (authorize_url + SCOPED_AUTHORIZE_URL) + l_result.replace_substring_all ("$CLIENT_ID", config.api_key.as_string_8) + if attached config.callback as l_callback then + l_result.replace_substring_all ("$REDIRECT_URI", (create {OAUTH_ENCODER}).encoded_string (l_callback.as_string_8)) + end + end + end + +feature -- Implementation + + Scoped_authorize_url: STRING = "&scope=$SCOPE"; + + +end diff --git a/modules/login/persistence/cms_oauth_20_storage_i.e b/modules/login/persistence/cms_oauth_20_storage_i.e new file mode 100644 index 0000000..2357707 --- /dev/null +++ b/modules/login/persistence/cms_oauth_20_storage_i.e @@ -0,0 +1,66 @@ +note + description: "[ + API to handle OAUTH storage + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class + CMS_OAUTH_20_STORAGE_I + +inherit + SHARED_LOGGER + +feature -- Error Handling + + error_handler: ERROR_HANDLER + -- Error handler. + deferred + end + +feature -- Access: Users + + user_oauth2_by_id (a_uid: like {CMS_USER}.id; a_consumer_table: READABLE_STRING_32): detachable CMS_USER + -- Retrieve a user by id `a_uid' for the consumer `a_consumer', if aby. + deferred + end + + user_oauth2_by_token (a_token: READABLE_STRING_32; a_consumer_table: READABLE_STRING_32): detachable CMS_USER + -- Retrieve a user by token `a_token' for the consumer `a_consumer'. + deferred + end + + user_oauth2_without_consumer_by_token (a_token: READABLE_STRING_32 ): detachable CMS_USER + -- Retrieve a user by token `a_token' searching in all the registered consumers in the system. + deferred + end + +feature -- Access: Consumers + + oauth2_consumers: LIST [STRING] + deferred + end + + oauth_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER + -- Retrieve a consumer by name `a_name', if any. + deferred + end + + oauth_consumer_by_callback (a_callback: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER + -- Retrieve a consumer by callback `a_callback', if any. + deferred + end + +feature -- Change: User Oauth2 + + new_user_oauth2 (a_token: READABLE_STRING_32; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer_table: READABLE_STRING_32) + -- Add a new user with oauth2 authentication. + deferred + end + + update_user_oauth2 (a_token: READABLE_STRING_32; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer_table: READABLE_STRING_32 ) + -- Update user `a_user' with oauth2 authentication. + deferred + end + +end diff --git a/modules/login/persistence/cms_oauth_20_storage_null.e b/modules/login/persistence/cms_oauth_20_storage_null.e new file mode 100644 index 0000000..d15d092 --- /dev/null +++ b/modules/login/persistence/cms_oauth_20_storage_null.e @@ -0,0 +1,69 @@ +note + description: "Summary description for {CMS_OAUTH_20_STORAGE_NULL}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + CMS_OAUTH_20_STORAGE_NULL + +inherit + + CMS_OAUTH_20_STORAGE_I + + +feature -- Error handler + + error_handler: ERROR_HANDLER + -- Error handler. + do + create Result.make + end + +feature -- Access: Users + + user_oauth2_by_id (a_uid: like {CMS_USER}.id; a_consumer_table: READABLE_STRING_32): detachable CMS_USER + -- CMS User with Oauth credential by id if any. + do + end + + user_oauth2_by_token (a_token: READABLE_STRING_32; a_consumer_table: READABLE_STRING_32): detachable CMS_USER + -- -- CMS User with Oauth credential by access token `a_token' if any. + do + end + + user_oauth2_without_consumer_by_token (a_token: READABLE_STRING_32 ): detachable CMS_USER + do + end + +feature -- Access: Consumers + + oauth2_consumers: LIST [STRING] + do + create {ARRAYED_LIST[STRING]} Result.make (0) + end + + oauth_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER + -- Retrieve a consumer by name `a_name', if any. + do + end + + oauth_consumer_by_callback (a_callback: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER + -- Retrieve a consumer by callback `a_callback', if any. + do + end + +feature -- Change: User Oauth2 + + new_user_oauth2 (a_token: READABLE_STRING_32; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer_table: READABLE_STRING_32) + -- Add a new user with oauth2 authentication. + do + end + + update_user_oauth2 (a_token: READABLE_STRING_32; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer_table: READABLE_STRING_32 ) + -- Update user `a_user' with oauth2 authentication. + do + end + + +end diff --git a/modules/login/persistence/cms_oauth_20_storage_sql.e b/modules/login/persistence/cms_oauth_20_storage_sql.e new file mode 100644 index 0000000..ffbad07 --- /dev/null +++ b/modules/login/persistence/cms_oauth_20_storage_sql.e @@ -0,0 +1,293 @@ +note + description: "Summary description for {CMS_OAUTH_20_STORAGE_SQL}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_OAUTH_20_STORAGE_SQL + +inherit + CMS_OAUTH_20_STORAGE_I + + CMS_PROXY_STORAGE_SQL + + CMS_OAUTH_20_STORAGE_I + + CMS_STORAGE_SQL_I + + REFACTORING_HELPER + +create + make + +feature -- Access User Outh + + user_oauth2_without_consumer_by_token (a_token: READABLE_STRING_32 ): detachable CMS_USER + -- Retrieve a user by token `a_token' searching in all the registered consumers in the system. + local + l_list: LIST[STRING] + do + error_handler.reset + write_information_log (generator + ".user_oauth2_without_consumer_by_token") + l_list := oauth2_consumers + from + l_list.start + until + l_list.after or attached Result + loop + if attached {CMS_USER} user_oauth2_by_token (a_token, l_list.item) as l_user then + Result := l_user + end + l_list.forth + end + end + + user_oauth2_by_id (a_uid: like {CMS_USER}.id; a_consumer: READABLE_STRING_32): detachable CMS_USER + -- + local + l_parameters: STRING_TABLE [detachable ANY] + l_string: STRING + do + error_handler.reset + write_information_log (generator + ".user_oauth2_by_id") + create l_parameters.make (1) + l_parameters.put (a_uid, "uid") + create l_string.make_from_string (select_user_oauth2_template_by_id) + l_string.replace_substring_all ("$table_name", sql_table_name (a_consumer)) + sql_query (l_string, l_parameters) + if sql_rows_count = 1 then + Result := fetch_user + else + check no_more_than_one: sql_rows_count = 0 end + end + end + + user_oauth2_by_token (a_token: READABLE_STRING_32; a_consumer: READABLE_STRING_32): detachable CMS_USER + -- + local + l_parameters: STRING_TABLE [detachable ANY] + l_string: STRING + do + error_handler.reset + write_information_log (generator + ".user_by_oauth2_token") + create l_parameters.make (1) + l_parameters.put (a_token, "token") + create l_string.make_from_string (select_user_by_oauth2_template_token) + l_string.replace_substring_all ("$table_name", sql_table_name (a_consumer)) + sql_query (l_string, l_parameters) + if sql_rows_count = 1 then + Result := fetch_user + else + check no_more_than_one: sql_rows_count = 0 end + end + end + + +feature --Access: Consumers + + oauth2_consumers: LIST[STRING] + -- Return a list of consumers, or empty + do + error_handler.reset + create {ARRAYED_LIST[STRING]}Result.make (0) + write_information_log (generator + ".user_by_oauth2_token") + sql_query (Sql_oauth_consumers, Void) + if not has_error then + from + sql_start + until + sql_after + loop + if attached sql_read_string (1) as l_name then + Result.force (l_name) + end + sql_forth + end + end + end + + oauth_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER + -- Retrieve a consumer by name `a_name', if any. + local + l_parameters: STRING_TABLE [detachable ANY] + do + error_handler.reset + write_information_log (generator + ".oauth_consumer_by_name") + create l_parameters.make (1) + l_parameters.put (a_name, "name") + sql_query (sql_oauth_consumer_name, l_parameters) + if sql_rows_count = 1 then + Result := fetch_consumer + else + check no_more_than_one: sql_rows_count = 0 end + end + end + + oauth_consumer_by_callback (a_callback: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER + -- Retrieve a consumer by callback `a_callback', if any. + local + l_parameters: STRING_TABLE [detachable ANY] + do + error_handler.reset + write_information_log (generator + ".oauth_consumer_by_callback") + create l_parameters.make (1) + l_parameters.put (a_callback, "name") + sql_query (sql_oauth_consumer_callback, l_parameters) + if sql_rows_count = 1 then + Result := fetch_consumer + else + check no_more_than_one: sql_rows_count = 0 end + end + end + +feature -- Change: User OAuth + + new_user_oauth2 (a_token: READABLE_STRING_32; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer: READABLE_STRING_32) + -- Add a new user with oauth2 authentication. + -- . + local + l_parameters: STRING_TABLE [detachable ANY] + l_string: STRING + do + error_handler.reset + sql_begin_transaction + + write_information_log (generator + ".new_user_oauth2") + create l_parameters.make (4) + l_parameters.put (a_user.id, "uid") + l_parameters.put (a_token, "token") + l_parameters.put (a_user_profile, "profile") + l_parameters.put (create {DATE_TIME}.make_now_utc, "utc_date") + + create l_string.make_from_string (sql_insert_oauth2_template) + l_string.replace_substring_all ("$table_name", sql_table_name (a_consumer)) + sql_change (l_string, l_parameters) + sql_commit_transaction + end + + update_user_oauth2 (a_token: READABLE_STRING_32; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer: READABLE_STRING_32 ) + -- Update user `a_user' with oauth2 authentication. + -- + local + l_parameters: STRING_TABLE [detachable ANY] + l_string: STRING + do + error_handler.reset + sql_begin_transaction + + write_information_log (generator + ".new_user_oauth2") + create l_parameters.make (4) + l_parameters.put (a_user.id, "uid") + l_parameters.put (a_token, "token") + l_parameters.put (a_user_profile, "profile") + + create l_string.make_from_string (sql_update_oauth2_template) + l_string.replace_substring_all ("$table_name", sql_table_name (a_consumer)) + sql_change (l_string, l_parameters) + sql_commit_transaction + end + +feature {NONE} -- Implementation OAuth Consumer + + fetch_consumer: detachable CMS_OAUTH_20_CONSUMER + do + if attached sql_read_integer_64 (1) as l_id then + create Result + Result.set_id (l_id) + end + if Result /= Void then + if attached sql_read_string_32 (2) as l_name then + Result.set_name (l_name) + end + if attached sql_read_string_32 (3) as l_api_secret then + Result.set_api_secret (l_api_secret) + end + if attached sql_read_string_32 (4) as l_api_key then + Result.set_api_key (l_api_key) + end + if attached sql_read_string_32 (5) as l_scope then + Result.set_scope (l_scope) + end + if attached sql_read_string_32 (6) as l_resource_url then + Result.set_protected_resource_url (l_resource_url) + end + if attached sql_read_string_32 (7) as l_callback_name then + Result.set_callback_name (l_callback_name) + end + if attached sql_read_string_32 (8) as l_extractor then + Result.set_extractor (l_extractor) + end + if attached sql_read_string_32 (9) as l_authorize_url then + Result.set_authorize_url (l_authorize_url) + end + if attached sql_read_string_32 (10) as l_endpoint then + Result.set_endpoint (l_endpoint) + end + end + end +feature {NONE} -- Implementation: User + + fetch_user: detachable CMS_USER + local + l_id: INTEGER_64 + l_name: detachable READABLE_STRING_32 + do + if attached sql_read_integer_32 (1) as i then + l_id := i + end + if attached sql_read_string_32 (2) as s and then not s.is_whitespace then + l_name := s + end + + if l_name /= Void then + create Result.make (l_name) + if l_id > 0 then + Result.set_id (l_id) + end + elseif l_id > 0 then + create Result.make_with_id (l_id) + end + + if Result /= Void then + if attached sql_read_string (3) as l_password then + -- FIXME: should we return the password here ??? + Result.set_hashed_password (l_password) + end + if attached sql_read_string (5) as l_email then + Result.set_email (l_email) + end + if attached sql_read_integer_32 (6) as l_status then + Result.set_status (l_status) + end + else + check expected_valid_user: False end + end + end + +feature -- {NONE} User OAuth2 + + sql_table_name (a_consumer: READABLE_STRING_8): STRING_8 + do + Result := Sql_table_prefix.twin + Result.append (a_consumer) + end + + Select_user_by_oauth2_template_token: STRING = "SELECT u.* FROM users as u JOIN $table_name as og ON og.uid = u.uid and og.access_token = :token;" + + Select_user_oauth2_template_by_id: STRING = "SELECT u.* FROM users as u JOIN $table_name as og ON og.uid = u.uid and og.uid = :uid;" + + Sql_insert_oauth2_template: STRING = "INSERT INTO $table_name (uid, access_token, details, created) VALUES (:uid, :token, :profile, :utc_date);" + + Sql_update_oauth2_template: STRING = "UPDATE $table_name SET access_token = :token, details = :profile WHERE uid =:uid;" + + Sql_oauth_consumers: STRING = "SELECT name FROM oauth2_consumers"; + + Sql_table_prefix: STRING = "oauth2_" + +feature -- {NONE} Consumer + + Sql_oauth_consumer_callback: STRING ="SELECT * FROM oauth2_consumers where callback_name =:name;" + + Sql_oauth_consumer_name: STRING ="SELECT * FROM oauth2_consumers where name =:name;" + +end diff --git a/src/persistence/sql/cms_storage_sql_builder.e b/src/persistence/sql/cms_storage_sql_builder.e index a1bec3b..6323b86 100644 --- a/src/persistence/sql/cms_storage_sql_builder.e +++ b/src/persistence/sql/cms_storage_sql_builder.e @@ -36,6 +36,7 @@ feature -- Initialization create u.make ("admin") u.set_password ("istrator#") u.set_email (a_setup.site_email) + u.set_status ({CMS_USER}.active) a_storage.new_user (u) --| Node @@ -74,16 +75,19 @@ feature -- Initialization create u.make ("auth") u.set_password ("enticated#") u.set_email (a_setup.site_email) + u.set_status ({CMS_USER}.active) a_storage.new_user (u) create u.make ("test") u.set_password ("test#") u.set_email (a_setup.site_email) + u.set_status ({CMS_USER}.active) a_storage.new_user (u) create u.make ("view") u.set_password ("only#") u.set_email (a_setup.site_email) + u.set_status ({CMS_USER}.active) u.set_roles (l_roles) a_storage.new_user (u) end diff --git a/src/persistence/sql/cms_storage_sql_i.e b/src/persistence/sql/cms_storage_sql_i.e index 6a1e903..6c779fa 100644 --- a/src/persistence/sql/cms_storage_sql_i.e +++ b/src/persistence/sql/cms_storage_sql_i.e @@ -76,7 +76,7 @@ feature -- Operation i := a_sql_statement.index_of (':', i) if i = 0 then i := n -- exit - else + elseif a_sql_statement.at (i-1).is_equal ('%'') or else a_sql_statement.at (i-1).is_equal ('%"') or else a_sql_statement.at (i-1).is_equal (' ') or else a_sql_statement.at (i-1).is_equal ('=') then from j := i + 1 until @@ -124,6 +124,30 @@ feature -- Operation feature -- Helper + sql_execute_file_script_with_params (a_path: PATH; a_params: detachable STRING_TABLE [detachable ANY]) + -- Execute SQL script from `a_path' and with params `a_params'. + local + f: PLAIN_TEXT_FILE + sql: STRING + do + create f.make_with_path (a_path) + if f.exists and then f.is_access_readable then + create sql.make (f.count) + f.open_read + from + f.start + until + f.exhausted or f.end_of_file + loop + f.read_stream_thread_aware (1_024) + sql.append (f.last_string) + end + f.close + sql_execute_script_with_params (sql, a_params) + end + end + + sql_execute_file_script (a_path: PATH) -- Execute SQL script from `a_path'. local @@ -181,6 +205,14 @@ feature -- Helper end end + sql_execute_script_with_params (a_sql_script: STRING; a_params: detachable STRING_TABLE [detachable ANY]) + -- Execute SQL script. + -- i.e: multiple SQL statements. + do + reset_error + sql_change (a_sql_script, a_params) + end + sql_table_exists (a_table_name: READABLE_STRING_8): BOOLEAN -- Does table `a_table_name' exists? do @@ -364,6 +396,26 @@ feature {NONE} -- Implementation loop c := a_script[i] inspect c + when '-' then + if i < n and then a_script[i + 1] = '-' then + -- Commented line "--" until New Line + j := a_script.index_of ('%N', i) + if j > 0 then + i := j + else + i := n + end + end + when '/' then + if i < n and then a_script[i + 1] = '*' then + -- Commented text "/*" until closing "*/" + j := a_script.substring_index ("*/", i) + if j > 0 then + i := j + else + i := n + end + end when '`', '"', '%'' then from j := i diff --git a/src/persistence/user/cms_user_storage_i.e b/src/persistence/user/cms_user_storage_i.e index e31fd14..bd82eda 100644 --- a/src/persistence/user/cms_user_storage_i.e +++ b/src/persistence/user/cms_user_storage_i.e @@ -56,6 +56,20 @@ feature -- Access password: Result /= Void implies (Result.hashed_password /= Void and Result.password = Void) end + user_by_activation_token (a_token: READABLE_STRING_32): detachable CMS_USER + -- User with activation token `a_token', if any. + deferred + ensure + password: Result /= Void implies (Result.hashed_password /= Void and Result.password = Void) + end + + user_by_password_token (a_token: READABLE_STRING_32): detachable CMS_USER + -- User with password token `a_token', if any. + deferred + ensure + password: Result /= Void implies (Result.hashed_password /= Void and Result.password = Void) + end + is_valid_credential (a_u, a_p: READABLE_STRING_32): BOOLEAN -- Does account with username `a_username' and password `a_password' exist? deferred @@ -141,4 +155,28 @@ feature -- Change: roles and permissions deferred end +feature -- Change: User activation + + save_activation (a_token: READABLE_STRING_32; a_id: INTEGER_64) + -- . + deferred + end + + remove_activation (a_token: READABLE_STRING_32) + -- . + deferred + end + +feature -- Change: User password recovery + + save_password (a_token: READABLE_STRING_32; a_id: INTEGER_64) + -- . + deferred + end + + remove_password (a_token: READABLE_STRING_32) + -- . + deferred + end + end diff --git a/src/persistence/user/cms_user_storage_null.e b/src/persistence/user/cms_user_storage_null.e index 7422618..283fe5b 100644 --- a/src/persistence/user/cms_user_storage_null.e +++ b/src/persistence/user/cms_user_storage_null.e @@ -34,6 +34,14 @@ feature -- Access: user do end + user_by_activation_token (a_token: READABLE_STRING_32): detachable CMS_USER + do + end + + user_by_password_token (a_token: READABLE_STRING_32): detachable CMS_USER + do + end + is_valid_credential (l_auth_login, l_auth_password: READABLE_STRING_32): BOOLEAN do end @@ -76,4 +84,28 @@ feature -- Change: roles and permissions do end +feature -- Change: User activation + + save_activation (a_token: READABLE_STRING_32; a_id: INTEGER_64) + -- . + do + end + + remove_activation (a_token: READABLE_STRING_32) + -- . + do + end + +feature -- Change: User password recovery + + save_password (a_token: READABLE_STRING_32; a_id: INTEGER_64) + -- . + do + end + + remove_password (a_token: READABLE_STRING_32) + -- . + do + end + end diff --git a/src/persistence/user/cms_user_storage_sql_i.e b/src/persistence/user/cms_user_storage_sql_i.e index 54c26a0..58e4723 100644 --- a/src/persistence/user/cms_user_storage_sql_i.e +++ b/src/persistence/user/cms_user_storage_sql_i.e @@ -62,7 +62,7 @@ feature -- Access: user l_parameters: STRING_TABLE [detachable ANY] do error_handler.reset - write_information_log (generator + ".user") + write_information_log (generator + ".user_by_id") create l_parameters.make (1) l_parameters.put (a_id, "uid") sql_query (select_user_by_id, l_parameters) @@ -107,6 +107,40 @@ feature -- Access: user end end + user_by_activation_token (a_token: READABLE_STRING_32): detachable CMS_USER + -- User for the given activation token `a_token', if any. + local + l_parameters: STRING_TABLE [detachable ANY] + do + error_handler.reset + write_information_log (generator + ".user_by_activation_token") + create l_parameters.make (1) + l_parameters.put (a_token, "token") + sql_query (select_user_by_activation_token, l_parameters) + if sql_rows_count = 1 then + Result := fetch_user + else + check no_more_than_one: sql_rows_count = 0 end + end + end + + user_by_password_token (a_token: READABLE_STRING_32): detachable CMS_USER + -- User for the given password token `a_token', if any. + local + l_parameters: STRING_TABLE [detachable ANY] + do + error_handler.reset + write_information_log (generator + ".user_by_password_token") + create l_parameters.make (1) + l_parameters.put (a_token, "token") + sql_query (select_user_by_password_token, l_parameters) + if sql_rows_count = 1 then + Result := fetch_user + else + check no_more_than_one: sql_rows_count = 0 end + end + end + is_valid_credential (l_auth_login, l_auth_password: READABLE_STRING_32): BOOLEAN local l_security: SECURITY_PROVIDER @@ -155,6 +189,7 @@ feature -- Change: user l_parameters.put (l_password_salt, "salt") l_parameters.put (l_email, "email") l_parameters.put (create {DATE_TIME}.make_now_utc, "created") + l_parameters.put (a_user.status, "status") sql_change (sql_insert_user, l_parameters) if not error_handler.has_error then @@ -197,6 +232,7 @@ feature -- Change: user l_parameters.put (l_password_salt, "salt") l_parameters.put (l_email, "email") l_parameters.put (create {DATE_TIME}.make_now_utc, "changed") + l_parameters.put (a_user.status, "status") sql_change (sql_update_user, l_parameters) else @@ -441,6 +477,107 @@ feature -- Change: roles and permissions end end + +feature -- Access: User activation + + activation_elapsed_time (a_token: READABLE_STRING_32): INTEGER_32 + -- amount of time that has passed in days since the token `a_token' was saved. + local + l_parameters: STRING_TABLE [detachable ANY] + do + error_handler.reset + write_information_log (generator + ".activation_elapsed_time") + create l_parameters.make (1) + l_parameters.put (a_token, "token") + sql_query (sql_select_activation_expiration, l_parameters) + if sql_rows_count = 1 then + Result := sql_read_integer_32 (1) + end + end + + user_id_by_activation (a_token: READABLE_STRING_32): INTEGER_64 + -- User id associatied with a token `a_token', if any. + local + l_parameters: STRING_TABLE [detachable ANY] + do + error_handler.reset + write_information_log (generator + ".user_id_by_actication") + create l_parameters.make (1) + l_parameters.put (a_token, "token") + sql_query (sql_select_userid_activation, l_parameters) + if sql_rows_count = 1 then + Result := sql_read_integer_32 (1) + end + end + +feature -- Change: User activation + + save_activation (a_token: READABLE_STRING_32; a_id: INTEGER_64) + -- + local + l_parameters: STRING_TABLE [detachable ANY] + l_utc_date: DATE_TIME + do + error_handler.reset + sql_begin_transaction + write_information_log (generator + ".save_activation") + create l_utc_date.make_now_utc + create l_parameters.make (2) + l_parameters.put (a_token, "token") + l_parameters.put (a_id, "uid") + l_parameters.put (l_utc_date, "utc_date") + sql_change (sql_insert_activation, l_parameters) + sql_commit_transaction + end + + remove_activation (a_token: READABLE_STRING_32) + -- . + local + l_parameters: STRING_TABLE [detachable ANY] + do + error_handler.reset + sql_begin_transaction + write_information_log (generator + ".remove_activation") + create l_parameters.make (1) + l_parameters.put (a_token, "token") + sql_change (sql_remove_activation, l_parameters) + sql_commit_transaction + end + +feature -- Change: User password recovery + + save_password (a_token: READABLE_STRING_32; a_id: INTEGER_64) + -- + local + l_parameters: STRING_TABLE [detachable ANY] + l_utc_date: DATE_TIME + do + error_handler.reset + sql_begin_transaction + write_information_log (generator + ".save_password") + create l_utc_date.make_now_utc + create l_parameters.make (2) + l_parameters.put (a_token, "token") + l_parameters.put (a_id, "uid") + l_parameters.put (l_utc_date, "utc_date") + sql_change (sql_insert_password, l_parameters) + sql_commit_transaction + end + + remove_password (a_token: READABLE_STRING_32) + -- . + local + l_parameters: STRING_TABLE [detachable ANY] + do + error_handler.reset + sql_begin_transaction + write_information_log (generator + ".remove_password") + create l_parameters.make (1) + l_parameters.put (a_token, "token") + sql_change (sql_remove_password, l_parameters) + sql_commit_transaction + end + feature {NONE} -- Implementation: User user_salt (a_username: READABLE_STRING_32): detachable READABLE_STRING_8 @@ -489,6 +626,9 @@ feature {NONE} -- Implementation: User if attached sql_read_string (5) as l_email then Result.set_email (l_email) end + if attached sql_read_integer_32 (6) as l_status then + Result.set_status (l_status) + end else check expected_valid_user: False end end @@ -551,10 +691,11 @@ feature {NONE} -- Sql Queries: USER Select_salt_by_username: STRING = "SELECT salt FROM Users WHERE name =:name;" -- Retrieve salt by username if exists. - sql_insert_user: STRING = "INSERT INTO users (name, password, salt, email, created) VALUES (:name, :password, :salt, :email, :created);" - -- SQL Insert to add a new node. + sql_insert_user: STRING = "INSERT INTO users (name, password, salt, email, created, status) VALUES (:name, :password, :salt, :email, :created, :status);" + -- SQL Insert to add a new user. - sql_update_user: STRING = "UPDATE users SET name=:name, password=:password, salt=:salt, email=:email WHERE uid=:uid;" + sql_update_user: STRING = "UPDATE users SET name=:name, password=:password, salt=:salt, email=:email, status=:status WHERE uid=:uid;" + -- SQL update to update an existing user. feature {NONE} -- Sql Queries: USER ROLE @@ -584,4 +725,32 @@ feature {NONE} -- Sql Queries: USER ROLE select_role_permissions_by_role_id: STRING = "SELECT permission, module FROM role_permissions WHERE rid=:rid;" -- User role permissions for role id :rid; +feature {NONE} -- Sql Queries: USER ACTIVATION + + sql_insert_activation: STRING = "INSERT INTO users_activations (token, uid, created) VALUES (:token, :uid, :utc_date);" + -- SQL insert a new activation :token. + + sql_select_activation_expiration: STRING = "SELECT DATEDIFF(day,created,UTC_DATE()) FROM users_activations where token = :token;" + -- elapsed time that has passed in days since the token `a_token' was saved. + + sql_select_userid_activation: STRING = "SELECT uid FROM users_activations where token = :token;" + -- Retrieve userid given the activation token. + + Select_user_by_activation_token: STRING = "SELECT u.* FROM users as u JOIN users_activations as ua ON ua.uid = u.uid and ua.token = :token;" + -- Retrieve user by activation token if exist. + + Sql_remove_activation: STRING = "DELETE FROM users_activations WHERE token = :token;" + -- Remove activation token. + +feature {NONE} -- User Password Recovery + + sql_insert_password: STRING = "INSERT INTO users_password_recovery (token, uid, created) VALUES (:token, :uid, :utc_date);" + -- SQL insert a new password recovery :token. + + Sql_remove_password: STRING = "DELETE FROM users_password_recovery WHERE token = :token;" + -- Retrieve password if exist. + + Select_user_by_password_token: STRING = "SELECT u.* FROM users as u JOIN users_password_recovery as ua ON ua.uid = u.uid and ua.token = :token;" + -- Retrieve user by password token if exist. + end diff --git a/src/service/email/email_service.e b/src/service/email/email_service.e new file mode 100644 index 0000000..833aeb6 --- /dev/null +++ b/src/service/email/email_service.e @@ -0,0 +1,102 @@ +note + description: "Basic Email Service" + date: "$Date: 2015-04-30 05:45:25 -0300 (ju. 30 de abr. de 2015) $" + revision: "$Revision: 97218 $" + +class + EMAIL_SERVICE + +inherit + + SHARED_ERROR + SHARED_LOGGER + +create + make + +feature {NONE} -- Initialization + + make (a_params: like parameters) + -- Create instance of {EMAIL_SERVICE} with smtp_server `a_params.smtp_server'. + -- Using `a_params.admin_email' as admin email. + do + parameters := a_params + initialize + end + + initialize + -- Initialize service. + local + l_address_factory: INET_ADDRESS_FACTORY + do + admin_email := parameters.admin_email + + -- Get local host name needed in creation of SMTP_PROTOCOL. + create l_address_factory + create smtp_protocol.make (parameters.smtp_server, l_address_factory.create_localhost.host_name) + set_successful + end + + parameters: EMAIL_SERVICE_PARAMETERS + -- Associated parameters. + + admin_email: IMMUTABLE_STRING_8 + -- Site admin's email. + + smtp_protocol: SMTP_PROTOCOL + -- SMTP protocol. + +feature -- Basic Operations + + send_internal_email (a_content: READABLE_STRING_GENERAL) + do + send_message (admin_email, admin_email, "Notification Contact", a_content) + end + + send_email_internal_server_error (a_content: READABLE_STRING_GENERAL) + do + send_message (admin_email, admin_email, "Internal Server Error", a_content) + end + + send_message (a_from_address, a_to_address: READABLE_STRING_8; a_subjet: READABLE_STRING_GENERAL; a_content: READABLE_STRING_GENERAL) + local + l_email: EMAIL + utf: UTF_CONVERTER + do + write_debug_log (generator + ".send_message: [from:" + a_from_address + ", to:" + a_to_address + ", subject:" + a_subjet + ", content:" + a_content) + create l_email.make_with_entry (a_from_address, a_to_address) + l_email.set_message (utf.escaped_utf_32_string_to_utf_8_string_8 (a_content)) + l_email.add_header_entry ({EMAIL_CONSTANTS}.H_subject, utf.escaped_utf_32_string_to_utf_8_string_8 (a_subjet)) + l_email.add_header_entry ("MIME-Version:", "1.0") + l_email.add_header_entry ("Content-Type", "text/html; charset=utf-8") + send_email (l_email) + end + +feature {NONE} -- Implementation + + send_email (a_email: EMAIL) + -- Send the email represented by `a_email'. + local + l_retried: BOOLEAN + do + if not l_retried then + write_information_log (generator + ".send_email Process send email.") + smtp_protocol.initiate_protocol + smtp_protocol.transfer (a_email) + smtp_protocol.close_protocol + write_information_log (generator + ".send_email Email sent.") + if smtp_protocol.error then + set_last_error ("smtp_protocol reported an error", generator + ".send_email") + else + set_successful + end + else + write_error_log (generator + ".send_email Email not send " + last_error_message ) + end + rescue + set_last_error_from_exception (generator + ".send_email") + l_retried := True + retry + end + +end diff --git a/src/service/email/email_service_parameters.e b/src/service/email/email_service_parameters.e new file mode 100644 index 0000000..c7d18e1 --- /dev/null +++ b/src/service/email/email_service_parameters.e @@ -0,0 +1,20 @@ +note + description: "Basic Email Service customized for cms site" + author: "" + date: "$Date: 2015-01-16 07:17:14 -0300 (vi. 16 de ene. de 2015) $" + revision: "$Revision: 96467 $" + +deferred class + EMAIL_SERVICE_PARAMETERS + +feature -- Access + + smtp_server: IMMUTABLE_STRING_8 + deferred + end + + admin_email: IMMUTABLE_STRING_8 + deferred + end + +end diff --git a/src/service/user/cms_user_api.e b/src/service/user/cms_user_api.e index 5f374df..8465f91 100644 --- a/src/service/user/cms_user_api.e +++ b/src/service/user/cms_user_api.e @@ -29,6 +29,24 @@ feature -- Access Result := storage.user_by_name (a_username) end + user_by_email (a_email: READABLE_STRING_32): detachable CMS_USER + -- User by email `a_email', if any. + do + Result := storage.user_by_email (a_email) + end + + user_by_activation_token (a_token: READABLE_STRING_32): detachable CMS_USER + -- User by activation token `a_token'. + do + Result := storage.user_by_activation_token (a_token) + end + + user_by_password_token (a_token: READABLE_STRING_32): detachable CMS_USER + -- User by password token `a_token'. + do + Result := storage.user_by_password_token (a_token) + end + feature -- Status report is_valid_credential (a_auth_login, a_auth_password: READABLE_STRING_32): BOOLEAN @@ -133,4 +151,43 @@ feature -- Change User storage.update_user (a_user) end +feature -- User Activation + + new_activation (a_token: READABLE_STRING_32; a_id: INTEGER_64) + -- Save activation token `a_token', for the user with the id `a_id'. + do + storage.save_activation (a_token, a_id) + end + + remove_activation (a_token: READABLE_STRING_32) + -- Remove activation token `a_token', from the storage. + do + storage.remove_activation (a_token) + end + +feature -- User Password Recovery + + new_password (a_token: READABLE_STRING_32; a_id: INTEGER_64) + -- Save password token `a_token', for the user with the id `a_id'. + do + storage.save_password (a_token, a_id) + end + + remove_password (a_token: READABLE_STRING_32) + -- Remove password token `a_token', from the storage. + do + storage.remove_password (a_token) + end + +feature -- User status + + not_active: INTEGER = 0 + -- The user is not active. + + active: INTEGER = 1 + -- The user is active + + Trashed: INTEGER = -1 + -- The user is trashed (soft delete), ready to be deleted/destroyed from storage. + end