diff --git a/.env b/.env new file mode 100644 index 0000000..af23d07 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres://postgres:postgres@localhost/postgres diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 18ed769..ad40002 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,8 +1,7 @@ name: Deploy to GOV.UK PaaS on: - push: - branches: [main] + workflow_dispatch: env: REGISTRY: ghcr.io diff --git a/Gemfile b/Gemfile index e5666f8..c2c8f27 100644 --- a/Gemfile +++ b/Gemfile @@ -7,3 +7,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } gem "sinatra" gem "pry" gem "thin" +gem "pg" +gem "sequel" +gem "dotenv" +gem "jwt" diff --git a/Gemfile.lock b/Gemfile.lock index cb931c0..0377d70 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,10 +3,13 @@ GEM specs: coderay (1.1.3) daemons (1.4.1) + dotenv (2.7.6) eventmachine (1.2.7) + jwt (2.3.0) method_source (1.0.0) mustermann (1.1.1) ruby2_keywords (~> 0.0.1) + pg (1.2.3) pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) @@ -14,6 +17,7 @@ GEM rack-protection (2.1.0) rack ruby2_keywords (0.0.5) + sequel (5.52.0) sinatra (2.1.0) mustermann (~> 1.0) rack (~> 2.2) @@ -29,7 +33,11 @@ PLATFORMS x86_64-linux DEPENDENCIES + dotenv + jwt + pg pry + sequel sinatra thin diff --git a/README.md b/README.md index 3b69d61..ef7a966 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # CIFU Forms API Prototype A prototype for a forms API for the collecting information from users team built with Ruby and Sinatra. + +## Running the database with docker + +`docker run --name db -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:13` diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..cbaf76f --- /dev/null +++ b/config.ru @@ -0,0 +1,5 @@ +RACK_ENV = ENV['RACK_ENV'] ||= 'development' unless defined?(RACK_ENV) +require_relative 'loader' +require_relative './server' + +run Server diff --git a/db/database.rb b/db/database.rb new file mode 100644 index 0000000..60c1b91 --- /dev/null +++ b/db/database.rb @@ -0,0 +1,40 @@ +require "sequel" + +class Migrator + def initialize + Sequel.extension :migration + end + + def destroy(database) + Sequel::Migrator.run(database, "#{__dir__}/migrations", target: 0) + end + + def migrate(database) + Sequel::Migrator.run(database, "#{__dir__}/migrations") + end + + def migrate_to(database, version) + Sequel::Migrator.run(database, "#{__dir__}/migrations", target: version) + end +end + +class Database + def initialize + @migrator = Migrator.new + end + + def connect + database = Sequel.connect(ENV['DATABASE_URL']) + load_extensions_for(database) + + @migrator.migrate(database) + database + end + + private + + def load_extensions_for(database) + database.extension :pg_json + database.extension :pg_array + end +end diff --git a/db/migrations/1_add_forms_table.rb b/db/migrations/1_add_forms_table.rb new file mode 100644 index 0000000..9c3b649 --- /dev/null +++ b/db/migrations/1_add_forms_table.rb @@ -0,0 +1,11 @@ +Sequel.migration do + change do + create_table :forms do + primary_key :id, type: :Bignum + String :username + String :key + String :display_name + column :form, 'json' + end + end +end diff --git a/example_forms/get-condition-evaluation-context b/example_forms/get-condition-evaluation-context new file mode 100644 index 0000000..015d4bd --- /dev/null +++ b/example_forms/get-condition-evaluation-context @@ -0,0 +1,678 @@ +{ + "startPage": "/uk-passport", + "pages": [ + { + "path": "/uk-passport", + "components": [ + { + "type": "YesNoField", + "name": "ukPassport", + "title": "Do you have a UK passport?", + "options": { + "required": true + }, + "schema": { + + } + } + ], + "section": "checkBeforeYouStart", + "next": [ + { + "path": "/how-many-people" + }, + { + "path": "/anothertest", + "condition": "WLkGmF" + }, + { + "path": "/testconditions", + "condition": "doesntHaveUKPassport" + } + ], + "title": "Do you have a UK passport?" + }, + { + "path": "/how-many-people", + "section": "applicantDetails", + "components": [ + { + "options": { + "classes": "govuk-input--width-10", + "required": true + }, + "type": "SelectField", + "name": "numberOfApplicants", + "title": "How many applicants are there?", + "list": "numberOfApplicants", + "schema": { + + } + } + ], + "next": [ + { + "path": "/applicant-one-name" + } + ], + "title": "How many applicants are there?" + }, + { + "path": "/applicant-one-name", + "title": "Applicant 1", + "section": "applicantOneDetails", + "components": [ + { + "type": "Para", + "content": "Provide the details as they appear on your passport.", + "options": { + "required": true + }, + "schema": { + + } + }, + { + "type": "TextField", + "name": "firstName", + "title": "First name", + "options": { + "required": true + }, + "schema": { + + } + }, + { + "options": { + "required": false, + "optionalText": false + }, + "type": "TextField", + "name": "middleName", + "title": "Middle name", + "hint": "If you have a middle name on your passport you must include it here", + "schema": { + + } + }, + { + "type": "TextField", + "name": "lastName", + "title": "Surname", + "options": { + "required": true + }, + "schema": { + + } + } + ], + "next": [ + { + "path": "/applicant-one-address" + } + ] + }, + { + "path": "/applicant-one-address", + "section": "applicantOneDetails", + "components": [ + { + "type": "UkAddressField", + "name": "address", + "title": "Address", + "options": { + "required": true + }, + "schema": { + + } + } + ], + "next": [ + { + "path": "/applicant-two", + "condition": "moreThanOneApplicant" + }, + { + "path": "/contact-details" + } + ], + "title": "Address" + }, + { + "path": "/applicant-two", + "title": "Applicant 2", + "section": "applicantTwoDetails", + "components": [ + { + "type": "Para", + "content": "Provide the details as they appear on your passport.", + "options": { + "required": true + }, + "schema": { + + } + }, + { + "type": "TextField", + "name": "firstName", + "title": "First name", + "options": { + "required": true + }, + "schema": { + + } + }, + { + "options": { + "required": false, + "optionalText": false + }, + "type": "TextField", + "name": "middleName", + "title": "Middle name", + "hint": "If you have a middle name on your passport you must include it here", + "schema": { + + } + }, + { + "type": "TextField", + "name": "lastName", + "title": "Surname", + "options": { + "required": true + }, + "schema": { + + } + } + ], + "next": [ + { + "path": "/applicant-two-address" + } + ] + }, + { + "path": "/applicant-two-address", + "section": "applicantTwoDetails", + "components": [ + { + "type": "UkAddressField", + "name": "address", + "title": "Address", + "options": { + "required": true + }, + "schema": { + + } + } + ], + "next": [ + { + "path": "/applicant-three", + "condition": "moreThanTwoApplicants" + }, + { + "path": "/contact-details" + } + ], + "title": "Address" + }, + { + "path": "/applicant-three", + "title": "Applicant 3", + "section": "applicantThreeDetails", + "components": [ + { + "type": "Para", + "content": "Provide the details as they appear on your passport.", + "options": { + "required": true + }, + "schema": { + + } + }, + { + "type": "TextField", + "name": "firstName", + "title": "First name", + "options": { + "required": true + }, + "schema": { + + } + }, + { + "options": { + "required": false, + "optionalText": false + }, + "type": "TextField", + "name": "middleName", + "title": "Middle name", + "hint": "If you have a middle name on your passport you must include it here", + "schema": { + + } + }, + { + "type": "TextField", + "name": "lastName", + "title": "Surname", + "options": { + "required": true + }, + "schema": { + + } + } + ], + "next": [ + { + "path": "/applicant-three-address" + } + ] + }, + { + "path": "/applicant-three-address", + "section": "applicantThreeDetails", + "components": [ + { + "type": "UkAddressField", + "name": "address", + "title": "Address", + "options": { + "required": true + }, + "schema": { + + } + } + ], + "next": [ + { + "path": "/applicant-four", + "condition": "moreThanThreeApplicants" + }, + { + "path": "/contact-details" + } + ], + "title": "Address" + }, + { + "path": "/applicant-four", + "title": "Applicant 4", + "section": "applicantFourDetails", + "components": [ + { + "type": "Para", + "content": "Provide the details as they appear on your passport.", + "options": { + "required": true + }, + "schema": { + + } + }, + { + "type": "TextField", + "name": "firstName", + "title": "First name", + "options": { + "required": true + }, + "schema": { + + } + }, + { + "options": { + "required": false, + "optionalText": false + }, + "type": "TextField", + "name": "middleName", + "title": "Middle name", + "hint": "If you have a middle name on your passport you must include it here", + "schema": { + + } + }, + { + "type": "TextField", + "name": "lastName", + "title": "Surname", + "options": { + "required": true + }, + "schema": { + + } + } + ], + "next": [ + { + "path": "/applicant-four-address" + } + ] + }, + { + "path": "/applicant-four-address", + "section": "applicantFourDetails", + "components": [ + { + "type": "UkAddressField", + "name": "address", + "title": "Address", + "options": { + "required": true + }, + "schema": { + + } + } + ], + "next": [ + { + "path": "/contact-details" + } + ], + "title": "Address" + }, + { + "path": "/contact-details", + "section": "applicantDetails", + "components": [ + { + "type": "TelephoneNumberField", + "name": "phoneNumber", + "title": "Phone number", + "hint": "If you haven't got a UK phone number, include country code", + "options": { + "required": true + }, + "schema": { + + } + }, + { + "type": "EmailAddressField", + "name": "emailAddress", + "title": "Your email address", + "options": { + "required": true + }, + "schema": { + + } + } + ], + "next": [ + { + "path": "/testconditions" + } + ], + "title": "Applicant contact details" + }, + { + "path": "/summary", + "controller": "./pages/summary.js", + "title": "Summary", + "components": [ + + ], + "next": [ + + ] + }, + { + "path": "/testconditions", + "title": "TestConditions", + "components": [ + { + "name": "pmmRYP", + "options": { + "condition": "bDDfgf" + }, + "type": "Para", + "content": "There Is Someone Called Applicant", + "schema": { + + } + } + ], + "next": [ + { + "path": "/summary" + } + ] + }, + { + "path": "/anothertest", + "title": "AnotherTest", + "components": [ + { + "name": "FnMsqX", + "options": { + + }, + "type": "Para", + "content": "Another Page" + } + ], + "next": [ + { + "path": "/summary" + } + ] + } + ], + "lists": [ + { + "name": "numberOfApplicants", + "title": "Number of people", + "type": "number", + "items": [ + { + "text": "1", + "value": 1, + "description": "", + "condition": "" + }, + { + "text": "2", + "value": 2, + "description": "", + "condition": "" + }, + { + "text": "3", + "value": 3, + "description": "", + "condition": "" + }, + { + "text": "4", + "value": 4, + "description": "", + "condition": "" + } + ] + } + ], + "sections": [ + { + "name": "checkBeforeYouStart", + "title": "Check before you start" + }, + { + "name": "applicantDetails", + "title": "Applicant details" + }, + { + "name": "applicantOneDetails", + "title": "Applicant 1" + }, + { + "name": "applicantTwoDetails", + "title": "Applicant 2" + }, + { + "name": "applicantThreeDetails", + "title": "Applicant 3" + }, + { + "name": "applicantFourDetails", + "title": "Applicant 4" + } + ], + "phaseBanner": { + + }, + "fees": [ + + ], + "outputs": [ + { + "name": "Ric43H5Ctwl4NBDC9x1_4", + "title": "email", + "type": "email", + "outputConfiguration": { + "emailAddress": "jennifermyanh.duong@digital.homeoffice.gov.uk" + } + } + ], + "declaration": "
All the answers you have provided are true to the best of your knowledge.
", + "version": 2, + "conditions": [ + { + "name": "hasUKPassport", + "displayName": "hasUKPassport", + "value": "checkBeforeYouStart.ukPassport==true" + }, + { + "name": "moreThanOneApplicant", + "displayName": "moreThanOneApplicant", + "value": "applicantDetails.numberOfApplicants > 1" + }, + { + "name": "moreThanTwoApplicants", + "displayName": "moreThanTwoApplicants", + "value": "applicantDetails.numberOfApplicants > 2" + }, + { + "name": "moreThanThreeApplicants", + "displayName": "moreThanThreeApplicants", + "value": "applicantDetails.numberOfApplicants > 3" + }, + { + "displayName": "Another", + "name": "nAQyYp", + "value": { + "name": "Another", + "conditions": [ + { + "conditionName": "doesntHaveUKPassport", + "conditionDisplayName": "doesntHaveUKPassport" + } + ] + } + }, + { + "displayName": "Another2", + "name": "QFdzTQ", + "value": { + "name": "Another2", + "conditions": [ + { + "field": { + "name": "checkBeforeYouStart.ukPassport", + "type": "YesNoField", + "display": "Do you have a UK passport?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "true", + "display": "true" + } + } + ] + } + }, + { + "displayName": "Another3", + "name": "WLkGmF", + "value": { + "name": "Another3", + "conditions": [ + { + "field": { + "name": "applicantOneDetails.firstName", + "type": "TextField", + "display": "First name" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "Applicant", + "display": "Applicant" + } + }, + { + "coordinator": "and", + "field": { + "name": "checkBeforeYouStart.ukPassport", + "type": "YesNoField", + "display": "Do you have a UK passport?" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "false", + "display": "false" + } + } + ] + } + }, + { + "name": "bDDfgf", + "displayName": "testCondition", + "value": { + "name": "testCondition", + "conditions": [ + { + "field": { + "name": "applicantOneDetails.firstName", + "type": "TextField", + "display": "First name" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "Applicant", + "display": "Applicant" + } + } + ] + } + }, + { + "name": "doesntHaveUKPassport", + "displayName": "doesntHaveUKPassport", + "value": "checkBeforeYouStart.ukPassport==false" + } + ], + "skipSummary": false +} diff --git a/example_forms/report-a-terrorist b/example_forms/report-a-terrorist new file mode 100644 index 0000000..d2eabf6 --- /dev/null +++ b/example_forms/report-a-terrorist @@ -0,0 +1,281 @@ +{ + "startPage": "/do-you-have-a-link-to-the-evidence", + "pages": [ + { + "title": "Do you have a link to the evidence?", + "path": "/do-you-have-a-link-to-the-evidence", + "components": [ + { + "name": "UjidZI", + "options": {}, + "type": "Para", + "content": "It’s helpful if you can send us links to the relevant pages, or posts if it was posted on social media.", + "schema": {} + }, + { + "name": "rfUYC", + "options": {}, + "type": "Details", + "title": "Help me find the link", + "content": "If you’re on a website, the link appears in the bar at the top of the page. An example of a link is, www.gov.uk/page/1234/content#.", + "schema": {} + }, + { + "type": "RadiosField", + "title": "Do you have a link to the material?", + "options": { + "hideTitle": true + }, + "name": "doyouhavealink", + "schema": {}, + "list": "HTbt4V" + } + ], + "next": [ + { + "path": "/do-you-have-any-evidence" + }, + { + "path": "/yes-i-have-a-link-to-the-material", + "condition": "b-NGgWvGISkJJLuzsJIjv" + } + ], + "section": "PMXq1s" + }, + { + "path": "/do-you-have-any-evidence", + "title": "Do you have any evidence?", + "components": [ + { + "name": "OQrrkG", + "options": {}, + "type": "Para", + "content": "This could be an image or video, for example. Evidence is helpful should the material be deleted before we can find it.It’s safe to save evidence to your device for the purpose of reporting it to us. We recommend deleting it afterwards.", + "schema": {} + }, + { + "name": "3jdOpV", + "options": {}, + "type": "Details", + "title": "Help me take a screenshot", + "content": "Try this:Press the Shift key (⇧), command (or Cmd), and 3The screenshot will be saved to your DesktopYou can now upload it to the formTry this:Press the Ctrl key and the switch window keyThe screenshot will be saved to your DownloadsYou can now upload it to the formIf that doesn’t work, try pressing Ctrl and F5.When viewing the material:Press the Prt Scr key (or similar) to take a copy of your screenPaste the image into Microsoft Paint or a similar applicationSave the file to your computerUpload the file to the formIf that doesn’t work, you may need to search for how to take screenshots on your particular computer model.", + "schema": {} + }, + { + "name": "LU6RMD", + "options": {}, + "type": "RadiosField", + "title": "Do you have any evidence?", + "schema": {}, + "list": "mdmRq9" + } + ], + "next": [ + { + "path": "/is-there-anything-else-you-can-tell-us" + }, + { + "path": "/yes-i-have-evidence", + "condition": "On5IOaSRDSyLs1G7-Dmdy" + } + ], + "section": "PMXq1s" + }, + { + "title": "summary", + "path": "/summary", + "controller": "./pages/summary.js", + "components": [] + }, + { + "path": "/is-there-anything-else-you-can-tell-us", + "title": "Is there anything else you can tell us?", + "components": [ + { + "name": "HETM3o", + "options": {}, + "type": "Para", + "content": "Details may include:who shared the materialwhen it was shareda description, if you haven’t provided a link or evidence", + "schema": {} + }, + { + "name": "evZ-IJ", + "options": { + "required": false + }, + "type": "MultilineTextField", + "title": "Additional Info", + "schema": {} + } + ], + "next": [ + { + "path": "/summary" + } + ], + "section": "PMXq1s" + }, + { + "path": "/yes-i-have-a-link-to-the-material", + "title": "Yes I have a link to the material", + "components": [ + { + "type": "MultilineTextField", + "title": "Link to the material", + "hint": "Please put in the link to the material here", + "name": "blarGGH", + "options": {}, + "schema": {} + } + ], + "next": [ + { + "path": "/do-you-have-any-evidence" + } + ], + "section": "PMXq1s" + }, + { + "path": "/yes-i-have-evidence", + "title": "Yes I have evidence", + "components": [ + { + "name": "koE_ae", + "options": { + "required": false + }, + "type": "FileUploadField", + "title": "Evidence File Upload", + "hint": "Please upload your evidence here", + "schema": {} + } + ], + "next": [ + { + "path": "/is-there-anything-else-you-can-tell-us" + } + ], + "section": "PMXq1s" + } + ], + "lists": [ + { + "title": "linktomateriallist", + "name": "HTbt4V", + "type": "string", + "items": [ + { + "text": "Yes, I do have a link", + "value": "yes" + }, + { + "text": "No, I don't have a link", + "value": "no" + } + ] + }, + { + "title": "evidencelist", + "name": "mdmRq9", + "type": "string", + "items": [ + { + "text": "Yes, I have evidence", + "value": "yes" + }, + { + "text": "No, I don't have evidence", + "value": "no" + } + ] + } + ], + "sections": [ + { + "name": "PMXq1s", + "title": "Evidence" + } + ], + "phaseBanner": {}, + "metadata": {}, + "fees": [], + "outputs": [ + { + "name": "q7mMOb6Eu6EauibjcTFT3", + "title": "powerapps", + "type": "webhook", + "outputConfiguration": { + "url": "https://prod-182.westeurope.logic.azure.com:443/workflows/cfa66b774dad40459bf36e334d860445/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=4nNQTOYk5DnEByQSKMb5jzu_cXJ0NKJuH4KZGBiqvMk" + } + } + ], + "version": 2, + "conditions": [ + { + "name": "b-NGgWvGISkJJLuzsJIjv", + "displayName": "hasLink", + "value": { + "name": "hasLink", + "conditions": [ + { + "field": { + "name": "PMXq1s.doyouhavealink", + "type": "RadiosField", + "display": "Do you have a link to the material? in PMXq1s" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "yes", + "display": "Yes, I do have a link" + } + } + ] + } + }, + { + "name": "xY51EDbc4lPr6kHZl1umG", + "displayName": "noEvidence", + "value": { + "name": "noEvidence", + "conditions": [ + { + "field": { + "name": "PMXq1s.LU6RMD", + "type": "RadiosField", + "display": "Do you have any evidence? in PMXq1s" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "no", + "display": "No, I don't have evidence" + } + } + ] + } + }, + { + "name": "On5IOaSRDSyLs1G7-Dmdy", + "displayName": "hasEvidence", + "value": { + "name": "hasEvidence", + "conditions": [ + { + "field": { + "name": "PMXq1s.LU6RMD", + "type": "RadiosField", + "display": "Do you have any evidence? in PMXq1s" + }, + "operator": "is", + "value": { + "type": "Value", + "value": "yes", + "display": "Yes, I have evidence" + } + } + ] + } + } + ] +} diff --git a/example_forms/runner-components-test b/example_forms/runner-components-test new file mode 100644 index 0000000..3831649 --- /dev/null +++ b/example_forms/runner-components-test @@ -0,0 +1,379 @@ +{ + "startPage": "/do-you-own-a-vehicle", + "pages": [ + { + "title": "Do you own a vehicle?", + "path": "/do-you-own-a-vehicle", + "components": [ + { + "name": "qqbRw1", + "options": {}, + "type": "YesNoField", + "title": "Do you own a vehicle?", + "values": { + "type": "listRef" + }, + "schema": {} + }, + { + "name": "LdOljB", + "options": {}, + "type": "InsetText", + "content": "It does not matter what you pick as it will redirect you to the same page", + "schema": {} + } + ], + "next": [ + { + "path": "/what-address-is-the-vehicle-registered-to" + } + ] + }, + { + "path": "/what-address-is-the-vehicle-registered-to", + "title": "What address is the vehicle registered to?", + "components": [ + { + "name": "sFR4aX", + "options": {}, + "type": "UkAddressField", + "title": "What address is the vehicle registered to?", + "schema": {} + }, + { + "name": "tVlnZa", + "options": {}, + "type": "DateField", + "title": "What date was the vehicle registered at this address?", + "schema": {} + }, + { + "name": "Z0Guyn", + "options": {}, + "type": "CheckboxesField", + "title": "Which Clean Air Zones are you claiming an exemption for?", + "list": "O2uEB1", + "hint": "Please check all that apply, however you are restricted to a maximum of two choices.", + "schema": {} + }, + { + "name": "M8gMK7", + "options": { + "required": false + }, + "type": "FileUploadField", + "title": "Proof of address", + "hint": "This can be a drivers licence or utility bill dated with the last month", + "schema": {} + } + ], + "next": [ + { + "path": "/details-about-your-vehicle" + } + ] + }, + { + "path": "/clean-air-zone-caz-exemption", + "title": "Clean Air Zone (CAZ) Exemption", + "components": [ + { + "name": "MOB13t", + "options": {}, + "type": "Para", + "content": "How to check if you're exempt from paying a charge and how to create a business account, and what support or exemptions are available. ", + "schema": {} + } + ], + "next": [ + { + "path": "/do-you-own-a-vehicle" + } + ], + "controller": "./pages/start.js" + }, + { + "path": "/details-about-your-vehicle", + "title": "Details about your vehicle", + "components": [ + { + "name": "0ZVmN_", + "options": {}, + "type": "AutocompleteField", + "title": "What is the make of you vehicle?", + "list": "-HMHHj", + "schema": {} + }, + { + "name": "gHSgo2", + "options": {}, + "type": "TextField", + "title": "Vehicle Model", + "hint": "For example A1, 740, Elantra", + "schema": {} + }, + { + "name": "4LZ9to", + "options": {}, + "type": "DatePartsField", + "title": "Date you purchased the vehicle?", + "schema": {} + }, + { + "type": "RadiosField", + "title": "What fuel type does your vehicle use?", + "list": "sm_ssM", + "name": "fsfsdfsdf", + "nameHasError": false, + "options": {}, + "schema": {} + }, + { + "name": "chYCuk", + "options": {}, + "type": "MultilineTextField", + "title": "Has the vehicle been modified in any way?", + "hint": "Failure to declare modifications will result in severe penalties if not disclosed at this point.", + "schema": {} + } + ], + "next": [ + { + "path": "/driver-details" + } + ] + }, + { + "path": "/driver-details", + "title": "Driver details", + "components": [ + { + "name": "wJzPKE", + "options": {}, + "type": "NumberField", + "title": "How many people in your household drive this vehicle?", + "schema": {} + }, + { + "name": "PNIThU", + "options": {}, + "type": "TextField", + "title": "Full name of the main driver", + "hint": "Please exclude your title", + "schema": {} + }, + { + "name": "0zL5bB", + "options": {}, + "type": "TelephoneNumberField", + "title": "Contact number", + "hint": "Landline or mobile", + "schema": {} + } + ], + "next": [ + { + "path": "/final-steps" + } + ] + }, + { + "path": "/final-steps", + "title": "final steps", + "components": [ + { + "name": "fkd8av", + "options": {}, + "type": "List", + "title": "Declaration", + "list": "mJHWaC", + "schema": {} + }, + { + "name": "L_2AYe", + "options": {}, + "type": "EmailAddressField", + "title": "Your email address", + "hint": "We will send confirmation of your application to the provided email address", + "schema": {} + } + ], + "next": [ + { + "path": "/summary" + } + ] + }, + { + "path": "/summary", + "title": "Summary", + "components": [], + "next": [], + "controller": "./pages/summary.js" + } + ], + "lists": [ + { + "title": "Vehicle Type", + "name": "ckrDmV", + "type": "string", + "items": [ + { + "text": "Car", + "value": 1 + }, + { + "text": "4 x 4/SUV", + "value": 2 + }, + { + "text": "Van", + "value": 3 + }, + { + "text": "Motorbike/Moped", + "value": 4 + } + ] + }, + { + "title": "Vehicle Make", + "name": "-HMHHj", + "type": "string", + "items": [ + { + "text": "Alfa Romeo", + "value": 1 + }, + { + "text": "BMW", + "value": 2 + }, + { + "text": "Ford", + "value": 3 + }, + { + "text": "Citroen", + "value": 4 + }, + { + "text": "Nissan", + "value": 5 + }, + { + "text": "Honda", + "value": 6 + }, + { + "text": "Mercedes", + "value": 8 + }, + { + "text": "Audi", + "value": 9 + }, + { + "text": "Toyota", + "value": 10 + }, + { + "text": "Hyundai", + "value": 11 + }, + { + "text": "Kia", + "value": 12 + } + ] + }, + { + "title": "Fuel types", + "name": "sm_ssM", + "type": "string", + "items": [ + { + "text": "Diesel", + "value": 1 + }, + { + "text": "Electric", + "value": 2 + }, + { + "text": "Hydrogen", + "value": 3 + }, + { + "text": "Petrol", + "value": 4 + }, + { + "text": "Hybrid", + "value": 5 + } + ] + }, + { + "title": "CAZ Location", + "name": "O2uEB1", + "type": "string", + "items": [ + { + "text": "Bath", + "value": 1 + }, + { + "text": "Bristol", + "value": 2 + }, + { + "text": "Birmingham", + "value": 3 + }, + { + "text": "Cardiff", + "value": 4 + }, + { + "text": "Liverpool", + "value": 6 + }, + { + "text": "Leeds", + "value": 7 + }, + { + "text": "Manchester", + "value": 8 + } + ] + }, + { + "title": "Declaration", + "name": "mJHWaC", + "type": "string", + "items": [ + { + "text": "You are not a Robot", + "value": 1 + }, + { + "text": "You have not previously claimed an exemption", + "value": 2 + }, + { + "text": "You have not omitted or purposely witheld information that might be detrimental to you claim", + "value": 3 + } + ] + } + ], + "sections": [], + "phaseBanner": {}, + "metadata": {}, + "fees": [], + "outputs": [], + "version": 2, + "conditions": [] +} \ No newline at end of file diff --git a/example_forms/test b/example_forms/test new file mode 100644 index 0000000..59b6701 --- /dev/null +++ b/example_forms/test @@ -0,0 +1,509 @@ +{ + "startPage": "/start", + "pages": [ + { + "title": "Start", + "path": "/start", + "components": [], + "next": [ + { + "path": "/uk-passport" + } + ], + "controller": "./pages/start.js" + }, + { + "path": "/uk-passport", + "components": [ + { + "type": "YesNoField", + "name": "ukPassport", + "title": "Do you have a UK passport?", + "options": { + "required": true + }, + "schema": {} + } + ], + "section": "checkBeforeYouStart", + "next": [ + { + "path": "/how-many-people" + }, + { + "path": "/no-uk-passport", + "condition": "doesntHaveUKPassport" + } + ], + "title": "Do you have a UK passport?" + }, + { + "path": "/no-uk-passport", + "title": "You're not eligible for this service", + "components": [ + { + "type": "Para", + "content": "If you still think you're eligible please contact the Foreign and Commonwealth Office.", + "options": { + "required": true + }, + "schema": {} + } + ], + "next": [] + }, + { + "path": "/how-many-people", + "section": "applicantDetails", + "components": [ + { + "options": { + "classes": "govuk-input--width-10", + "required": true + }, + "type": "SelectField", + "name": "numberOfApplicants", + "title": "How many applicants are there?", + "list": "numberOfApplicants" + } + ], + "next": [ + { + "path": "/applicant-one" + } + ], + "title": "How many applicants are there?" + }, + { + "path": "/applicant-one", + "title": "Applicant 1", + "section": "applicantOneDetails", + "components": [ + { + "type": "Para", + "content": "Provide the details as they appear on your passport.", + "options": { + "required": true + }, + "schema": {} + }, + { + "type": "TextField", + "name": "firstName", + "title": "First name", + "options": { + "required": true + }, + "schema": {} + }, + { + "options": { + "required": false, + "optionalText": false + }, + "type": "TextField", + "name": "middleName", + "title": "Middle name", + "hint": "If you have a middle name on your passport you must include it here", + "schema": {} + }, + { + "type": "TextField", + "name": "lastName", + "title": "Surname", + "options": { + "required": true + }, + "schema": {} + } + ], + "next": [ + { + "path": "/applicant-one-address" + } + ] + }, + { + "path": "/applicant-one-address", + "section": "applicantOneDetails", + "components": [ + { + "type": "UkAddressField", + "name": "address", + "title": "Address", + "options": { + "required": true + }, + "schema": {} + } + ], + "next": [ + { + "path": "/applicant-two", + "condition": "moreThanOneApplicant" + }, + { + "path": "/contact-details" + } + ], + "title": "Address" + }, + { + "path": "/applicant-two", + "title": "Applicant 2", + "section": "applicantTwoDetails", + "components": [ + { + "type": "Para", + "content": "Provide the details as they appear on your passport.", + "options": { + "required": true + }, + "schema": {} + }, + { + "type": "TextField", + "name": "firstName", + "title": "First name", + "options": { + "required": true + }, + "schema": {} + }, + { + "options": { + "required": false, + "optionalText": false + }, + "type": "TextField", + "name": "middleName", + "title": "Middle name", + "hint": "If you have a middle name on your passport you must include it here", + "schema": {} + }, + { + "type": "TextField", + "name": "lastName", + "title": "Surname", + "options": { + "required": true + }, + "schema": {} + } + ], + "next": [ + { + "path": "/applicant-two-address" + } + ] + }, + { + "path": "/applicant-two-address", + "section": "applicantTwoDetails", + "components": [ + { + "type": "UkAddressField", + "name": "address", + "title": "Address", + "options": { + "required": true + }, + "schema": {} + } + ], + "next": [ + { + "path": "/applicant-three", + "condition": "moreThanTwoApplicants" + }, + { + "path": "/contact-details" + } + ], + "title": "Address" + }, + { + "path": "/applicant-three", + "title": "Applicant 3", + "section": "applicantThreeDetails", + "components": [ + { + "type": "Para", + "content": "Provide the details as they appear on your passport.", + "options": { + "required": true + }, + "schema": {} + }, + { + "type": "TextField", + "name": "firstName", + "title": "First name", + "options": { + "required": true + }, + "schema": {} + }, + { + "options": { + "required": false, + "optionalText": false + }, + "type": "TextField", + "name": "middleName", + "title": "Middle name", + "hint": "If you have a middle name on your passport you must include it here", + "schema": {} + }, + { + "type": "TextField", + "name": "lastName", + "title": "Surname", + "options": { + "required": true + }, + "schema": {} + } + ], + "next": [ + { + "path": "/applicant-three-address" + } + ] + }, + { + "path": "/applicant-three-address", + "section": "applicantThreeDetails", + "components": [ + { + "type": "UkAddressField", + "name": "address", + "title": "Address", + "options": { + "required": true + }, + "schema": {} + } + ], + "next": [ + { + "path": "/applicant-four", + "condition": "moreThanThreeApplicants" + }, + { + "path": "/contact-details" + } + ], + "title": "Address" + }, + { + "path": "/applicant-four", + "title": "Applicant 4", + "section": "applicantFourDetails", + "components": [ + { + "type": "Para", + "content": "Provide the details as they appear on your passport.", + "options": { + "required": true + }, + "schema": {} + }, + { + "type": "TextField", + "name": "firstName", + "title": "First name", + "options": { + "required": true + }, + "schema": {} + }, + { + "options": { + "required": false, + "optionalText": false + }, + "type": "TextField", + "name": "middleName", + "title": "Middle name", + "hint": "If you have a middle name on your passport you must include it here", + "schema": {} + }, + { + "type": "TextField", + "name": "lastName", + "title": "Surname", + "options": { + "required": true + }, + "schema": {} + } + ], + "next": [ + { + "path": "/applicant-four-address" + } + ] + }, + { + "path": "/applicant-four-address", + "section": "applicantFourDetails", + "components": [ + { + "type": "UkAddressField", + "name": "address", + "title": "Address", + "options": { + "required": true + }, + "schema": {} + } + ], + "next": [ + { + "path": "/contact-details" + } + ], + "title": "Address" + }, + { + "path": "/contact-details", + "section": "applicantDetails", + "components": [ + { + "type": "TelephoneNumberField", + "name": "phoneNumber", + "title": "Phone number", + "hint": "If you haven't got a UK phone number, include country code", + "options": { + "required": true + }, + "schema": {} + }, + { + "type": "EmailAddressField", + "name": "emailAddress", + "title": "Your email address", + "options": { + "required": true + }, + "schema": {} + } + ], + "next": [ + { + "path": "/summary" + } + ], + "title": "Applicant contact details" + }, + { + "path": "/summary", + "controller": "./pages/summary.js", + "title": "Summary", + "components": [], + "next": [] + } + ], + "lists": [ + { + "name": "numberOfApplicants", + "title": "Number of people", + "type": "number", + "items": [ + { + "text": "1", + "value": 1, + "description": "", + "condition": "" + }, + { + "text": "2", + "value": 2, + "description": "", + "condition": "" + }, + { + "text": "3", + "value": 3, + "description": "", + "condition": "" + }, + { + "text": "4", + "value": 4, + "description": "", + "condition": "" + } + ] + } + ], + "sections": [ + { + "name": "checkBeforeYouStart", + "title": "Check before you start" + }, + { + "name": "applicantDetails", + "title": "Applicant details" + }, + { + "name": "applicantOneDetails", + "title": "Applicant 1" + }, + { + "name": "applicantTwoDetails", + "title": "Applicant 2" + }, + { + "name": "applicantThreeDetails", + "title": "Applicant 3" + }, + { + "name": "applicantFourDetails", + "title": "Applicant 4" + } + ], + "phaseBanner": {}, + "fees": [], + "payApiKey": "", + "outputs": [ + { + "name": "Ric43H5Ctwl4NBDC9x1_4", + "title": "email", + "type": "email", + "outputConfiguration": { + "emailAddress": "jennifermyanh.duong@digital.homeoffice.gov.uk" + } + } + ], + "declaration": "All the answers you have provided are true to the best of your knowledge.
", + "version": 2, + "conditions": [ + { + "name": "hasUKPassport", + "displayName": "hasUKPassport", + "value": "checkBeforeYouStart.ukPassport==true" + }, + { + "name": "doesntHaveUKPassport", + "displayName": "doesntHaveUKPassport", + "value": "checkBeforeYouStart.ukPassport==false" + }, + { + "name": "moreThanOneApplicant", + "displayName": "moreThanOneApplicant", + "value": "applicantDetails.numberOfApplicants > 1" + }, + { + "name": "moreThanTwoApplicants", + "displayName": "moreThanTwoApplicants", + "value": "applicantDetails.numberOfApplicants > 2" + }, + { + "name": "moreThanThreeApplicants", + "displayName": "moreThanThreeApplicants", + "value": "applicantDetails.numberOfApplicants > 3" + } + ] +} diff --git a/loader.rb b/loader.rb new file mode 100644 index 0000000..0297418 --- /dev/null +++ b/loader.rb @@ -0,0 +1,2 @@ +require 'dotenv/load' + diff --git a/manifest.yml b/manifest.yml index 7903776..603b827 100644 --- a/manifest.yml +++ b/manifest.yml @@ -2,4 +2,6 @@ applications: - name: cifu-xgov-persistence-service memory: 256M - command: bundle exec rackup + command: bundle exec rackup -p $PORT + services: + - cifu-xgov-form-api-db diff --git a/server.rb b/server.rb index 303cb10..d662b6b 100644 --- a/server.rb +++ b/server.rb @@ -1,51 +1,142 @@ require "sinatra" require "json" require "pry" +require 'jwt' +require_relative "./db/database" -before do - content_type :json -end +class Server < Sinatra::Base + before do + content_type :json + @database = Database.new.connect + end -post "/publish" do - request_body = request.body.read - request = JSON.parse(request_body) + after do + @database.disconnect + end - id = request['id'] - config = {} - config = request['configuration'] if request['configuration'] - p config + post "/publish" do + user = authenticated_user - File.write("./forms/#{id}", JSON.dump(config)) + request_body = request.body.read + request = JSON.parse(request_body) - config.to_json -end + id = request['id'] + config = {} + config = request['configuration'] if request['configuration'] + + if form_exists_for_user?(user, id) + @database[:forms].where( + username: user, + key: id + ).update( + form: Sequel.pg_json(config) + ) + else + @database[:forms].insert( + username: user, + key: id, + display_name: id, + form: Sequel.pg_json(config) + ) + end + + config.to_json + end + + def form_exists_for_user?(user, key) + !@database[:forms].where(username: user, key: key).all.empty? + end -get "/published" do - files = [] - forms = Dir.entries("./forms").select { |filename| File.file?("./forms/#{filename}") } - forms.each do |form| - File.open("./forms/#{form}") do |f| - files << { - "Key": form, - "DisplayName": form, + get "/published" do + user = authenticated_user + forms = [] + + forms_for_user(user).each do |form| + forms << { + "Key": form[:key], + "DisplayName": form[:display_name], "FeedbackForm": false } end + + forms.to_json end - files.to_json -end + get "/published/:id" do + api_key = authenticated_user + user = api_key unless api_key.nil? || api_key.empty? -get "/published/:id" do - form_content = {} + form = @database[:forms].where(username: user, key: params['id']).first + + if form.nil? + response.status = 404 + return {}.to_json + end - File.open("./forms/#{params['id']}") do |f| - file_content = f.read - form_content = JSON.parse(file_content) + { + id: form[:key], + values: form[:form] + }.to_json end - { - id: params['id'], - values: form_content - }.to_json -end \ No newline at end of file + get "/seed/:user" do + seed_data_for_user(params['user']) + + @database[:forms].where(username: params['user']).select(:key).all.to_json + end + + get "/login" do + content_type :html + + erb :login + end + + post "/login" do + content_type :html + + payload = { user: params['name'] } + token = JWT.encode payload, nil, 'none' + + redirect "#{ENV['DESIGNER_URL']}/app/auth?token=#{token}" + end + + private + + def authenticated_user + token = request.env['HTTP_X_API_KEY'] + begin + decoded_token = JWT.decode token, nil, false + return decoded_token[0]["user"] + rescue + return nil + end + end + + def forms_for_user(user) + forms = @database[:forms].where(username: user).all + + if forms.empty? + seed_data_for_user(user) + return @database[:forms].where(username: user).all + end + + forms + end + + def seed_data_for_user(user) + forms = Dir.entries("./example_forms").select { |filename| File.file?("./example_forms/#{filename}") } + forms.map do |filename| + File.open("./example_forms/#{filename}") do |f| + file_content = f.read + if @database[:forms].where(username: user).where(key: filename).all.count == 0 + @database[:forms].insert( + username: user, + key: filename, + display_name: filename, + form: file_content + ) + end + end + end + end +end diff --git a/views/login.erb b/views/login.erb new file mode 100644 index 0000000..13008fe --- /dev/null +++ b/views/login.erb @@ -0,0 +1,10 @@ + + + + + +