From 4955fe695ccb4c916d48a39714dcc59b7b05d357 Mon Sep 17 00:00:00 2001 From: HooVee Date: Tue, 27 Sep 2016 21:13:00 +0300 Subject: [PATCH] Added reCaptcha to application form --- members/static/js/angular-recaptcha.js | 306 +++++++++++++++++++ members/static/js/application_controllers.js | 37 ++- members/templates/application_index.html | 12 +- members/views.py | 48 +++ sikweb/settings-sample.py | 14 + 5 files changed, 410 insertions(+), 7 deletions(-) create mode 100644 members/static/js/angular-recaptcha.js diff --git a/members/static/js/angular-recaptcha.js b/members/static/js/angular-recaptcha.js new file mode 100644 index 0000000..5b025ce --- /dev/null +++ b/members/static/js/angular-recaptcha.js @@ -0,0 +1,306 @@ +/** + * angular-recaptcha build:2016-04-05 + * https://github.com/vividcortex/angular-recaptcha + * Copyright (c) 2016 VividCortex +**/ + +/*global angular, Recaptcha */ +(function (ng) { + 'use strict'; + + ng.module('vcRecaptcha', []); + +}(angular)); + +/*global angular */ +(function (ng) { + 'use strict'; + + function throwNoKeyException() { + throw new Error('You need to set the "key" attribute to your public reCaptcha key. If you don\'t have a key, please get one from https://www.google.com/recaptcha/admin/create'); + } + + var app = ng.module('vcRecaptcha'); + + /** + * An angular service to wrap the reCaptcha API + */ + app.provider('vcRecaptchaService', function(){ + var provider = this; + var config = {}; + provider.onLoadFunctionName = 'vcRecaptchaApiLoaded'; + + /** + * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance. + * + * @since 2.5.0 + * @param defaults object which overrides the current defaults object. + */ + provider.setDefaults = function(defaults){ + angular.copy(config, defaults); + }; + + /** + * Sets the reCaptcha key which will be used by default is not specified in a specific directive instance. + * + * @since 2.5.0 + * @param siteKey the reCaptcha public key (refer to the README file if you don't know what this is). + */ + provider.setSiteKey = function(siteKey){ + config.key = siteKey; + }; + + /** + * Sets the reCaptcha theme which will be used by default is not specified in a specific directive instance. + * + * @since 2.5.0 + * @param theme The reCaptcha theme. + */ + provider.setTheme = function(theme){ + config.theme = theme; + }; + + /** + * Sets the reCaptcha stoken which will be used by default is not specified in a specific directive instance. + * + * @since 2.5.0 + * @param stoken The reCaptcha stoken. + */ + provider.setStoken = function(stoken){ + config.stoken = stoken; + }; + + /** + * Sets the reCaptcha size which will be used by default is not specified in a specific directive instance. + * + * @since 2.5.0 + * @param size The reCaptcha size. + */ + provider.setSize = function(size){ + config.size = size; + }; + + /** + * Sets the reCaptcha type which will be used by default is not specified in a specific directive instance. + * + * @since 2.5.0 + * @param type The reCaptcha type. + */ + provider.setType = function(type){ + config.type = type; + }; + + /** + * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance. + * + * @since 2.5.0 + * @param onLoadFunctionName string name which overrides the name of the onload function. Should match what is in the recaptcha script querystring onload value. + */ + provider.setOnLoadFunctionName = function(onLoadFunctionName){ + provider.onLoadFunctionName = onLoadFunctionName; + }; + + provider.$get = ['$rootScope','$window', '$q', function ($rootScope, $window, $q) { + var deferred = $q.defer(), promise = deferred.promise, recaptcha; + + $window.vcRecaptchaApiLoadedCallback = $window.vcRecaptchaApiLoadedCallback || []; + + var callback = function () { + recaptcha = $window.grecaptcha; + + deferred.resolve(recaptcha); + }; + + $window.vcRecaptchaApiLoadedCallback.push(callback); + + $window[provider.onLoadFunctionName] = function () { + $window.vcRecaptchaApiLoadedCallback.forEach(function(callback) { + callback(); + }); + }; + + + function getRecaptcha() { + if (!!recaptcha) { + return $q.when(recaptcha); + } + + return promise; + } + + function validateRecaptchaInstance() { + if (!recaptcha) { + throw new Error('reCaptcha has not been loaded yet.'); + } + } + + + // Check if grecaptcha is not defined already. + if (ng.isDefined($window.grecaptcha)) { + callback(); + } + + return { + + /** + * Creates a new reCaptcha object + * + * @param elm the DOM element where to put the captcha + * @param conf the captcha object configuration + * @throws NoKeyException if no key is provided in the provider config or the directive instance (via attribute) + */ + create: function (elm, conf) { + + conf.sitekey = conf.key || config.key; + conf.theme = conf.theme || config.theme; + conf.stoken = conf.stoken || config.stoken; + conf.size = conf.size || config.size; + conf.type = conf.type || config.type; + + if (!conf.sitekey || conf.sitekey.length !== 40) { + throwNoKeyException(); + } + return getRecaptcha().then(function (recaptcha) { + return recaptcha.render(elm, conf); + }); + }, + + /** + * Reloads the reCaptcha + */ + reload: function (widgetId) { + validateRecaptchaInstance(); + + // $log.info('Reloading captcha'); + recaptcha.reset(widgetId); + + // Let everyone know this widget has been reset. + $rootScope.$broadcast('reCaptchaReset', widgetId); + }, + + /** + * Gets the response from the reCaptcha widget. + * + * @see https://developers.google.com/recaptcha/docs/display#js_api + * + * @returns {String} + */ + getResponse: function (widgetId) { + validateRecaptchaInstance(); + + return recaptcha.getResponse(widgetId); + } + }; + + }]; + }); + +}(angular)); + +/*global angular, Recaptcha */ +(function (ng) { + 'use strict'; + + var app = ng.module('vcRecaptcha'); + + app.directive('vcRecaptcha', ['$document', '$timeout', 'vcRecaptchaService', function ($document, $timeout, vcRecaptcha) { + + return { + restrict: 'A', + require: "?^^form", + scope: { + response: '=?ngModel', + key: '=?', + stoken: '=?', + theme: '=?', + size: '=?', + type: '=?', + tabindex: '=?', + required: '=?', + onCreate: '&', + onSuccess: '&', + onExpire: '&' + }, + link: function (scope, elm, attrs, ctrl) { + scope.widgetId = null; + + if(ctrl && angular.isDefined(attrs.required)){ + scope.$watch('required', validate); + } + + var removeCreationListener = scope.$watch('key', function (key) { + var callback = function (gRecaptchaResponse) { + // Safe $apply + $timeout(function () { + scope.response = gRecaptchaResponse; + validate(); + + // Notify about the response availability + scope.onSuccess({response: gRecaptchaResponse, widgetId: scope.widgetId}); + }); + }; + + vcRecaptcha.create(elm[0], { + + callback: callback, + key: key, + stoken: scope.stoken || attrs.stoken || null, + theme: scope.theme || attrs.theme || null, + type: scope.type || attrs.type || null, + tabindex: scope.tabindex || attrs.tabindex || null, + size: scope.size || attrs.size || null, + 'expired-callback': expired + + }).then(function (widgetId) { + // The widget has been created + validate(); + scope.widgetId = widgetId; + scope.onCreate({widgetId: widgetId}); + + scope.$on('$destroy', destroy); + + scope.$on('reCaptchaReset', function(resetWidgetId){ + if(angular.isUndefined(resetWidgetId) || widgetId === resetWidgetId){ + scope.response = ""; + validate(); + } + }) + + }); + + // Remove this listener to avoid creating the widget more than once. + removeCreationListener(); + }); + + function destroy() { + if (ctrl) { + // reset the validity of the form if we were removed + ctrl.$setValidity('recaptcha', null); + } + + cleanup(); + } + + function expired(){ + scope.response = ""; + validate(); + + // Notify about the response availability + scope.onExpire({widgetId: scope.widgetId}); + } + + function validate(){ + if(ctrl){ + ctrl.$setValidity('recaptcha', scope.required === false ? null : Boolean(scope.response)); + } + } + + function cleanup(){ + // removes elements reCaptcha added. + angular.element($document[0].querySelectorAll('.pls-container')).parent().remove(); + } + } + }; + }]); + +}(angular)); diff --git a/members/static/js/application_controllers.js b/members/static/js/application_controllers.js index 9dd4655..615f82d 100644 --- a/members/static/js/application_controllers.js +++ b/members/static/js/application_controllers.js @@ -1,6 +1,6 @@ //app -app = angular.module('applicationApp', []); +app = angular.module('applicationApp', ['vcRecaptcha']); //tokens @@ -26,12 +26,37 @@ var notySuccess = notyfication('success',2500); //controllers -app.controller("applicationController", function($scope, $http, $location, $window) { +app.controller("applicationController", function($scope, $http, $location, $window, vcRecaptchaService) { $scope.member = {}; + $scope.response = null; + $scope.widgetId = null; + $scope.model = { + key: '6LevHAcUAAAAAA45B7c-7qja-2aSwHztr9xb4K2Z' + }; + $scope.setResponse = function(response) { + $scope.response = response; + }; + $scope.setWidgetId = function(widgetId) { + $scope.widgetId = widgetId; + }; + $scope.cbExpiration = function() { + vcRecaptchaService.reload($scope.widgetId); + $scope.response = null; + }; $scope.send = function() { - $http.post("/members/api/request", $scope.member).then(function(data){ - notySuccess("Hakemus lähetetty!"); - $window.location.href = "/application/"; - }); + var valid; + //server side validation + $scope.member.reCaptchaResponse = vcRecaptchaService.getResponse() + if(resp === "") { + alert("Ole hyvä ja täytä kuvavarmennus"); + } else { + $http.post("/members/api/request", $scope.member).then(function(data){ + notySuccess("Hakemus lähetetty!"); + $window.location.href = "/application/"; + }, function(data){ + notyError("Jokin meni vikaan. Yritä uudelleen."); + vcRecaptchaService.reload($scope.widgetId); + }); + } } }); \ No newline at end of file diff --git a/members/templates/application_index.html b/members/templates/application_index.html index b72ae74..cff91a2 100644 --- a/members/templates/application_index.html +++ b/members/templates/application_index.html @@ -18,7 +18,9 @@ {%load staticfiles %} - + + +

Killan jäseneksi liittyminen on helppoa ja hauskaa. Täytä vain alla oleva lomake.

@@ -59,6 +61,14 @@ +
diff --git a/members/views.py b/members/views.py index 585f9f8..be6cd2f 100644 --- a/members/views.py +++ b/members/views.py @@ -6,6 +6,32 @@ from django.http import HttpResponse, HttpResponseBadRequest from django.core.exceptions import ValidationError from members.models import Member, MemberRequest import json +from django.core.mail import send_mail +import requests +from django.conf import settings + +#function to validate reCaptcha +def validateReCaptcha(response): + values = { + 'secret': settings.GOOGLE_RECAPTCHA_SECRET_KEY, + 'response': response, + } + url = "https://www.google.com/recaptcha/api/siteverify" + headers = {'Content-type': 'application/json'} + resp = requests.post(url, data=json.dumps(values), headers=headers) + result = json.loads(resp.read()) + if not result["success"]: + return False + return True + +def sendmail(subject, message): + send_mail( + subject, + message, + 'no-reply@sahkoinsinoorikilta.fi', + ['viestintamestari@sahkoinsinoorikilta.fi'], + fail_silently=False + ) @ensure_csrf_cookie @require_http_methods(["GET"]) @@ -92,11 +118,33 @@ def member_requests(request, *args, **kwargs): def new_member_request(request, *args, **kwargs): try: data = json.loads(request.body.decode("utf-8")) + #get captcha response from member + captcha = data.pop("reCaptchaResponse", "") + #send response to google and check it out + captcha_ok = validateReCaptcha(captcha) + #if not ok, inform user + if not captcha_ok: + return HttpResponseBadRequest('{"error" : "Captcha not ok. Please try again."}') + #if ok continue mem = Member.create_from_dict(data) req = MemberRequest.objects.create(member=mem) + subject = 'New application' + message = 'You have new application\r\n' + message += 'Member info:\r\n' + message += 'First name: ' + mem.first_name + '\r\n' + message += 'Last name: ' + mem.last_name + '\r\n' + message += 'Email: ' + mem.email + '\r\n' + message += 'Place of residence: ' + mem.POR + '\r\n' + message += 'AYY-membership: ' + str(mem.AYY) + '\r\n' + message += 'To mail list: ' + str(mem.jas) + '\r\n' + message += 'Created: ' + mem.created.isoformat(' ') + '\r\n' + message += 'Please go to the http://sika.sahkoinsinoorikilta.fi/members/ and do something about it!\r\n' + sendmail(subject, message) return HttpResponse(json.dumps(mem.get_dict())) except ValueError: return HttpResponseBadRequest('{"error" : "Invalid parameters supplied"}') + except TimeoutError: + return HttpResponseBadRequest('{"error" : "Much error, no connection"}') @ensure_csrf_cookie @require_http_methods(["GET", "POST", "DELETE"]) diff --git a/sikweb/settings-sample.py b/sikweb/settings-sample.py index 58d96a3..a6650ab 100644 --- a/sikweb/settings-sample.py +++ b/sikweb/settings-sample.py @@ -106,6 +106,20 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +#Email +# https://sendgrid.com/docs/Integrate/Frameworks/django.html + +EMAIL_HOST = 'smtp.sendgrid.net' +EMAIL_HOST_USER = 'sendgrid_username' +EMAIL_HOST_PASSWORD = 'sendgrid_password' +EMAIL_PORT = 587 +EMAIL_USE_TLS = True + +#ReCaptcha +# http://www.yaconiello.com/blog/integrating-google-recaptcha-to-django/ + +GOOGLE_RECAPTCHA_SITE_KEY = "YOUR-PUBLIC-KEY" +GOOGLE_RECAPTCHA_SECRET_KEY = "YOUR-PRIVATE-KEY" # Internationalization # https://docs.djangoproject.com/en/1.9/topics/i18n/