├── README.md ├── composer.json ├── config ├── install │ └── lambda_form.config.yml └── schema │ └── lambda_form.schema.yml ├── js └── form-handler.js ├── lambda_form.info.yml ├── lambda_form.libraries.yml ├── lambda_form.links.menu.yml ├── lambda_form.routing.yml ├── serverless ├── .gitignore ├── handler.js ├── secrets.example.json └── serverless.yml └── src └── Form ├── LambdaForm.php └── SettingsForm.php /README.md: -------------------------------------------------------------------------------- 1 | # Lambda Form 2 | 3 | Lambda from is an example module to show how a Drupal Form can be connected to a 4 | lambda function in order to handle the form in a serverless way. 5 | 6 | This is a proof of concept to be extended in real world static projects built 7 | using [Tome](https://www.drupal.org/project/tome) module. 8 | 9 | ## Contents 10 | * Drupal module providing a basic contact form 11 | * [Serverless](https://serverless.com) Lambda function. It sends an email with 12 | the data entered in the from above. 13 | 14 | ## Setup 15 | 16 | ### Lambda function 17 | This repository contains a `serverless` folder where it is included the 18 | serverless template and the nodejs code of a basic lambda function. 19 | 20 | In order to deploy this lambda function, you need to have an AWS account and 21 | serverless installed in your machine.To setup serverless you need to: 22 | * Install the serverless framwork `npm i -g serverless` 23 | * Setup your AWS CLI credentials in `~/.aws/credentials` file 24 | 25 | Then it is necessary to configure the app. You need to rename 26 | `secrets.example.json` to `secrets.json` and replace the variables. 27 | 28 | Finally run `serverless deploy`. If everything goes well, you will get the URL 29 | of the lambda endpoint. Something like 30 | `https://{id}.execute-api.{region}.amazonaws.com/{stage}/email/send`. This URl 31 | Will be used in the Drupal module to connect both parts. 32 | 33 | **NOTE**: This is a very brief summary. It is encouraged to read this 34 | [post](https://dev.to/adnanrahic/building-a-serverless-contact-form-with-aws-lambda-and-aws-ses-4jm0) 35 | , from where I took most of the ideas to build this function. 36 | 37 | ### Drupal module 38 | * Add this repository to your composer.json file : 39 | ``` 40 | { 41 | "type": "vcs", 42 | "url": "https://github.com/plopesc/lambda_form.git" 43 | } 44 | ``` 45 | * Run `composer require drupal/lambda_form` 46 | * Enable the module as usual 47 | * Visit http://yourdomain.com/admin/config/development/lambda_form and enter 48 | your lambda endpoint URL 49 | * Go to http://yourdomain.com/admin/lambda-form 50 | * Fill the form and enjoy! 51 | 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal/lambda_form", 3 | "type": "drupal-module", 4 | "description": "Lambda form example integration", 5 | "keywords": ["Drupal"], 6 | "license": "GPL-2.0+", 7 | "homepage": "https://github.com/plopesc/lambda_form", 8 | "minimum-stability": "dev", 9 | "support": { 10 | "issues": "https://github.com/plopesc/lambda_form/issues", 11 | "source": "https://github.com/plopesc/lambda_form" 12 | }, 13 | "require": { } 14 | } 15 | -------------------------------------------------------------------------------- /config/install/lambda_form.config.yml: -------------------------------------------------------------------------------- 1 | endpoint: '' 2 | -------------------------------------------------------------------------------- /config/schema/lambda_form.schema.yml: -------------------------------------------------------------------------------- 1 | # Schema for the configuration files of the Lambda Form. 2 | 3 | lambda_form.settings: 4 | type: config_object 5 | label: 'Lambda Form Settings' 6 | mapping: 7 | endpoint: 8 | type: uri 9 | label: 'The lambda endpoint URL' 10 | -------------------------------------------------------------------------------- /js/form-handler.js: -------------------------------------------------------------------------------- 1 | 2 | (function ($, Drupal, drupalSettings) { 3 | 4 | 'use strict'; 5 | 6 | Drupal.behaviors.lambdaForm = { 7 | attach: function (context, settings) { 8 | const form = document.getElementById('lambda-form'); 9 | const url = form.action; 10 | const toast = document.getElementById('edit-toast'); 11 | const submit = document.getElementById('edit-submit'); 12 | 13 | function post(url, body, callback) { 14 | var req = new XMLHttpRequest(); 15 | req.open("POST", url, true); 16 | req.setRequestHeader("Content-Type", "application/json"); 17 | req.addEventListener("load", function () { 18 | if (req.status < 400) { 19 | callback(null, JSON.parse(req.responseText)); 20 | } else { 21 | console.log('Request failed: ' + req.responseText); 22 | const message = (req.status < 500) ? req.responseText : 'There was an error with sending your message, hold up until I fix it. Thanks for waiting.'; 23 | callback(new Error(message)); 24 | } 25 | }); 26 | req.send(JSON.stringify(body)); 27 | } 28 | 29 | function success () { 30 | toast.innerHTML = 'Thanks for sending me a message! I\'ll get in touch with you ASAP. :)'; 31 | submit.disabled = false; 32 | submit.blur(); 33 | form.name.focus(); 34 | form.name.value = ''; 35 | form.email.value = ''; 36 | form.content.value = ''; 37 | } 38 | 39 | function error (err) { 40 | toast.innerHTML = err.message; 41 | submit.disabled = false; 42 | } 43 | 44 | $('form#lambda-form').once('lambda.form').each(function () { 45 | form.addEventListener('submit', function (e) { 46 | e.preventDefault(); 47 | toast.innerHTML = 'Sending'; 48 | submit.disabled = true; 49 | 50 | const payload = { 51 | name: form.name.value, 52 | email: form.email.value, 53 | content: form.content.value 54 | }; 55 | post(url, payload, function (err, res) { 56 | if (err) { 57 | return error(err) 58 | } 59 | success(); 60 | }) 61 | }) 62 | }); 63 | }, 64 | 65 | }; 66 | 67 | })(jQuery, Drupal, drupalSettings); 68 | -------------------------------------------------------------------------------- /lambda_form.info.yml: -------------------------------------------------------------------------------- 1 | name: 'Lambda form' 2 | type: module 3 | description: 'Lambda form example integration' 4 | core: 8.x 5 | package: 'Lambda' 6 | -------------------------------------------------------------------------------- /lambda_form.libraries.yml: -------------------------------------------------------------------------------- 1 | form-handler: 2 | js: 3 | js/form-handler.js: {} 4 | dependencies: 5 | - core/jquery.once -------------------------------------------------------------------------------- /lambda_form.links.menu.yml: -------------------------------------------------------------------------------- 1 | lambda_form.lambda_form_config: 2 | title: 'Lambda Form' 3 | route_name: lambda_form.lambda_form_config 4 | description: 'A description for the menu entry' 5 | parent: system.admin_config_development 6 | weight: 99 7 | 8 | -------------------------------------------------------------------------------- /lambda_form.routing.yml: -------------------------------------------------------------------------------- 1 | 2 | lambda_form.lambda_form: 3 | path: '/lambda-form' 4 | defaults: 5 | _form: '\Drupal\lambda_form\Form\LambdaForm' 6 | _title: 'Lambda form integration example' 7 | requirements: 8 | _access: 'TRUE' 9 | 10 | 11 | lambda_form.lambda_form_config: 12 | path: '/admin/config/development/lambda_form' 13 | defaults: 14 | _form: '\Drupal\lambda_form\Form\SettingsForm' 15 | _title: 'Lambda form settings' 16 | requirements: 17 | _permission: 'access administration pages' 18 | options: 19 | _admin_route: TRUE 20 | -------------------------------------------------------------------------------- /serverless/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | #Secrets file 9 | secrets.json 10 | -------------------------------------------------------------------------------- /serverless/handler.js: -------------------------------------------------------------------------------- 1 | const aws = require('aws-sdk'); 2 | const ses = new aws.SES(); 3 | const myEmail = process.env.EMAIL; 4 | const myDomain = process.env.DOMAIN; 5 | 6 | function generateResponse (code, payload) { 7 | return { 8 | statusCode: code, 9 | headers: { 10 | 'Access-Control-Allow-Origin': myDomain, 11 | 'Access-Control-Allow-Headers': 'x-requested-with', 12 | 'Access-Control-Allow-Credentials': true 13 | }, 14 | body: JSON.stringify(payload) 15 | } 16 | } 17 | 18 | function generateError (err) { 19 | const code = err.statusCode || 500; 20 | console.log(err); 21 | return { 22 | statusCode: code, 23 | headers: { 24 | 'Access-Control-Allow-Origin': myDomain, 25 | 'Access-Control-Allow-Headers': 'x-requested-with', 26 | 'Access-Control-Allow-Credentials': true 27 | }, 28 | body: err.message 29 | } 30 | } 31 | 32 | function generateEmailParams (body) { 33 | const { email, name, content } = JSON.parse(body); 34 | console.log(email, name, content); 35 | if (!(email && name && content)) { 36 | const error = new Error('Missing parameters! Make sure to add parameters \'Name\', \'Email\', \'Content\'.'); 37 | error.statusCode = 400; 38 | throw error; 39 | } 40 | 41 | return { 42 | Source: myEmail, 43 | Destination: { ToAddresses: [myEmail] }, 44 | ReplyToAddresses: [email], 45 | Message: { 46 | Body: { 47 | Text: { 48 | Charset: 'UTF-8', 49 | Data: `Message sent from email ${email} by ${name} \nContent: ${content}` 50 | } 51 | }, 52 | Subject: { 53 | Charset: 'UTF-8', 54 | Data: `You received a message from ${myDomain}!` 55 | } 56 | } 57 | } 58 | } 59 | 60 | module.exports.send = async (event) => { 61 | try { 62 | const emailParams = generateEmailParams(event.body); 63 | const data = await ses.sendEmail(emailParams).promise(); 64 | return generateResponse(200, data) 65 | } catch (err) { 66 | return generateError(err) 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /serverless/secrets.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "NODE_ENV":"dev", 3 | "EMAIL":"mymail@mail.com", 4 | "DOMAIN":"*" 5 | } 6 | -------------------------------------------------------------------------------- /serverless/serverless.yml: -------------------------------------------------------------------------------- 1 | service: lambda-form 2 | 3 | custom: 4 | secrets: ${file(secrets.json)} 5 | 6 | provider: 7 | name: aws 8 | runtime: nodejs8.10 9 | stage: ${self:custom.secrets.NODE_ENV} 10 | region: us-east-1 11 | environment: 12 | NODE_ENV: ${self:custom.secrets.NODE_ENV} 13 | EMAIL: ${self:custom.secrets.EMAIL} 14 | DOMAIN: ${self:custom.secrets.DOMAIN} 15 | iamRoleStatements: 16 | - Effect: "Allow" 17 | Action: 18 | - "ses:SendEmail" 19 | Resource: "*" 20 | 21 | functions: 22 | send: 23 | handler: handler.send 24 | events: 25 | - http: 26 | path: email/send 27 | method: post 28 | cors: true 29 | -------------------------------------------------------------------------------- /src/Form/LambdaForm.php: -------------------------------------------------------------------------------- 1 | 'textfield', 26 | '#title' => $this->t('Name'), 27 | '#required' => TRUE, 28 | '#maxlength' => 64, 29 | '#size' => 64, 30 | ]; 31 | 32 | $form['email'] = [ 33 | '#type' => 'email', 34 | '#required' => TRUE, 35 | '#title' => $this->t('Email'), 36 | ]; 37 | 38 | $form['content'] = [ 39 | '#type' => 'textarea', 40 | '#required' => TRUE, 41 | '#title' => $this->t('Content'), 42 | ]; 43 | 44 | $form['toast'] = [ 45 | '#type' => 'container', 46 | '#attributes' => ['class' => ['toast']], 47 | ]; 48 | 49 | $form['actions'] = ['#type' => 'actions']; 50 | $form['actions']['submit'] = [ 51 | '#type' => 'submit', 52 | '#value' => $this->t('Submit'), 53 | ]; 54 | 55 | $form['#action'] = $this->config('lambda_form.settings')->get('endpoint'); 56 | $form['#attached']['library'][] = 'lambda_form/form-handler'; 57 | 58 | return $form; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function submitForm(array &$form, FormStateInterface $form_state) { 65 | // Display result. 66 | foreach ($form_state->getValues() as $key => $value) { 67 | drupal_set_message($key . ': ' . $value); 68 | } 69 | 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/Form/SettingsForm.php: -------------------------------------------------------------------------------- 1 | config('lambda_form.settings'); 34 | $form['endpoint'] = [ 35 | '#type' => 'url', 36 | '#title' => $this->t('Endpoint URL'), 37 | '#description' => $this->t('The Lambda endpoint URL'), 38 | '#maxlength' => 128, 39 | '#size' => 128, 40 | '#default_value' => $config->get('endpoint'), 41 | ]; 42 | return parent::buildForm($form, $form_state); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function validateForm(array &$form, FormStateInterface $form_state) { 49 | parent::validateForm($form, $form_state); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function submitForm(array &$form, FormStateInterface $form_state) { 56 | parent::submitForm($form, $form_state); 57 | 58 | $this->config('lambda_form.settings') 59 | ->set('endpoint', $form_state->getValue('endpoint')) 60 | ->save(); 61 | } 62 | 63 | } 64 | --------------------------------------------------------------------------------