"
+```
diff --git a/recaptcha/demosite/app/Dockerfile b/recaptcha/demosite/app/Dockerfile
new file mode 100644
index 0000000000..f063624423
--- /dev/null
+++ b/recaptcha/demosite/app/Dockerfile
@@ -0,0 +1,18 @@
+FROM node:16-slim
+
+ARG GOOGLE_CLOUD_PROJECT
+ENV GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT}
+
+ARG SITE_KEY
+ENV SITE_KEY=${SITE_KEY}
+
+ARG CHECKBOX_SITE_KEY
+ENV CHECKBOX_SITE_KEY=${CHECKBOX_SITE_KEY}
+
+# Copy local code to the container image.
+ENV APP_HOME /app
+WORKDIR $APP_HOME
+COPY . ./
+
+# Install production dependencies.
+RUN npm install
diff --git a/recaptcha/demosite/app/controllers/assessmentController.js b/recaptcha/demosite/app/controllers/assessmentController.js
new file mode 100644
index 0000000000..35bccc606e
--- /dev/null
+++ b/recaptcha/demosite/app/controllers/assessmentController.js
@@ -0,0 +1,26 @@
+const {createAssessment} = require('../recaptcha/createAssessment');
+
+const assessmentController = async (req, res) => {
+ try {
+ const assessmentData = await createAssessment(
+ process.env.GOOGLE_CLOUD_PROJECT,
+ req.body.sitekey,
+ req.body.token,
+ req.body.action
+ );
+
+ res.json({
+ error: null,
+ data: assessmentData,
+ });
+ } catch (e) {
+ res.json({
+ error: e.toString(),
+ data: null,
+ });
+ }
+};
+
+module.exports = {
+ assessmentController,
+};
diff --git a/recaptcha/demosite/app/controllers/loginController.js b/recaptcha/demosite/app/controllers/loginController.js
new file mode 100644
index 0000000000..23201af7a8
--- /dev/null
+++ b/recaptcha/demosite/app/controllers/loginController.js
@@ -0,0 +1,12 @@
+const loginController = (req, res) => {
+ const context = {
+ project_id: process.env.GOOGLE_CLOUD_PROJECT,
+ site_key: process.env.SITE_KEY,
+ };
+
+ res.render('login', context);
+};
+
+module.exports = {
+ loginController,
+};
diff --git a/recaptcha/demosite/app/controllers/signupController.js b/recaptcha/demosite/app/controllers/signupController.js
new file mode 100644
index 0000000000..68ec096fa1
--- /dev/null
+++ b/recaptcha/demosite/app/controllers/signupController.js
@@ -0,0 +1,12 @@
+const signupController = (req, res) => {
+ const context = {
+ project_id: process.env.GOOGLE_CLOUD_PROJECT,
+ checkbox_site_key: process.env.CHECKBOX_SITE_KEY,
+ };
+
+ res.render('signup', context);
+};
+
+module.exports = {
+ signupController,
+};
diff --git a/recaptcha/demosite/app/index.js b/recaptcha/demosite/app/index.js
new file mode 100644
index 0000000000..acf01bacf3
--- /dev/null
+++ b/recaptcha/demosite/app/index.js
@@ -0,0 +1,26 @@
+const express = require('express');
+const mustacheExpress = require('mustache-express');
+const bodyParser = require('body-parser');
+
+const router = require('./routes');
+
+const app = express();
+const port = 8000;
+
+app.use(
+ bodyParser.urlencoded({
+ extended: true,
+ })
+);
+app.use(bodyParser.json());
+
+app.engine('html', mustacheExpress());
+app.set('view engine', 'html');
+app.set('views', __dirname + '/templates');
+
+app.use('/static', express.static('static'));
+app.use('/', router);
+
+app.listen(port, () => {
+ console.log(`Recaptcha demosite app listening on port ${port}`);
+});
diff --git a/recaptcha/demosite/app/init.sh b/recaptcha/demosite/app/init.sh
new file mode 100644
index 0000000000..03d661bb8d
--- /dev/null
+++ b/recaptcha/demosite/app/init.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+# gcloud command to get the current GOOGLE Project id.
+export GOOGLE_CLOUD_PROJECT=$(gcloud config list --format 'value(core.project)' 2>/dev/null)
+gcloud config set project "$GOOGLE_CLOUD_PROJECT"
+
+# Enabling the reCAPTCHA Enterprise API
+gcloud services enable recaptchaenterprise.googleapis.com
+
+# gcloud command to create reCAPTCHA keys.
+gcloud alpha recaptcha keys create --display-name=demo-recaptcha-score-key --web --allow-all-domains --integration-type=SCORE 1>/dev/null 2>recaptchascorekeyfile
+export SITE_KEY=$(cat recaptchascorekeyfile | sed -n -e 's/.*Created \[\([0-9a-zA-Z_-]\+\)\].*/\1/p')
+gcloud alpha recaptcha keys create --display-name=demo-recaptcha-checkbox-key --web --allow-all-domains --integration-type=CHECKBOX 1>/dev/null 2>recaptchacheckboxkeyfile
+export CHECKBOX_SITE_KEY=$(cat recaptchacheckboxkeyfile | sed -n -e 's/.*Created \[\([0-9a-zA-Z_-]\+\)\].*/\1/p')
+
+# Docker compose up
+DOCKER_COMPOSE="/usr/local/bin/docker-compose -f $HOME/cloudshell_open/nodejs-recaptcha-enterprise/samples/demosite/docker-compose.yaml up --build"
+$DOCKER_COMPOSE
+DOCKER_COMPOSE_RESULT=$?
+if [[ $DOCKER_COMPOSE_RESULT == *"error"* ]];
+then
+ echo "Deployment error"
+fi
diff --git a/recaptcha/demosite/app/package.json b/recaptcha/demosite/app/package.json
new file mode 100644
index 0000000000..aea82b880a
--- /dev/null
+++ b/recaptcha/demosite/app/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "recaptcha-demosite",
+ "description": "",
+ "version": "1.0.0",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/googleapis/nodejs-recaptcha-enterprise.git"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "@google-cloud/recaptcha-enterprise": "^3.0.0",
+ "body-parser": "^1.20.0",
+ "express": "^4.18.1",
+ "mustache-express": "^1.3.2"
+ }
+}
diff --git a/recaptcha/demosite/app/recaptcha/createAssessment.js b/recaptcha/demosite/app/recaptcha/createAssessment.js
new file mode 100644
index 0000000000..44af4a3971
--- /dev/null
+++ b/recaptcha/demosite/app/recaptcha/createAssessment.js
@@ -0,0 +1,63 @@
+const {RecaptchaEnterpriseServiceClient} =
+ require('@google-cloud/recaptcha-enterprise').v1;
+
+const THRESHHOLD_SCORE = 0.5;
+
+async function createAssessment(
+ projectId,
+ recaptchSiteKey,
+ token,
+ recaptchaAction
+) {
+ const client = new RecaptchaEnterpriseServiceClient();
+
+ const [response] = await client.createAssessment({
+ parent: `projects/${projectId}`,
+ assessment: {
+ event: {
+ siteKey: recaptchSiteKey,
+ token,
+ },
+ },
+ });
+
+ // Check if the token is valid.
+ if (!response.tokenProperties || !response.tokenProperties.valid) {
+ throw new Error(
+ `The Create Assessment call failed because the token was invalid for the following reasons: ${response.tokenProperties.invalidReason}`
+ );
+ }
+
+ // Check if the expected action was executed.
+ if (response.tokenProperties.action !== recaptchaAction) {
+ throw new Error(
+ 'The action attribute in your reCAPTCHA tag does not match the action you are expecting to score. Please check your action attribute !'
+ );
+ }
+
+ // Get the risk score and the reason(s)
+ // For more information on interpreting the assessment,
+ // see https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment
+ for (const reason of response.riskAnalysis.reasons) {
+ console.log(reason);
+ }
+
+ console.log(
+ `The reCAPTCHA score for this token is: ${response.riskAnalysis.score}`
+ );
+
+ let verdict = 'Human';
+
+ if (response.riskAnalysis.score < THRESHHOLD_SCORE) {
+ verdict = 'Not a human';
+ }
+
+ return {
+ score: response.riskAnalysis.score,
+ verdict,
+ };
+}
+
+module.exports = {
+ createAssessment,
+};
diff --git a/recaptcha/demosite/app/routes.js b/recaptcha/demosite/app/routes.js
new file mode 100644
index 0000000000..90a8274a2d
--- /dev/null
+++ b/recaptcha/demosite/app/routes.js
@@ -0,0 +1,12 @@
+const express = require('express');
+const router = express.Router();
+
+const {loginController} = require('./controllers/loginController');
+const {signupController} = require('./controllers/signupController');
+const {assessmentController} = require('./controllers/assessmentController');
+
+router.get('/login', loginController);
+router.get('/signup', signupController);
+router.post('/create_assessment', assessmentController);
+
+module.exports = router;
diff --git a/recaptcha/demosite/app/static/assessment.js b/recaptcha/demosite/app/static/assessment.js
new file mode 100644
index 0000000000..c8ee57ae19
--- /dev/null
+++ b/recaptcha/demosite/app/static/assessment.js
@@ -0,0 +1,94 @@
+const createAssessmentRequest = (token, action, sitekey) => {
+ return fetch('/create_assessment', {
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ method: 'POST',
+ body: JSON.stringify({token, action, sitekey}),
+ });
+};
+
+function assessRecaptcha(element, event) {
+ event.preventDefault();
+ grecaptcha.enterprise.ready(() => {
+ grecaptcha.enterprise
+ .execute(element.getAttribute('data-sitekey'), {
+ action: element.getAttribute('data-action'),
+ })
+ .then(token => {
+ return createAssessmentRequest(
+ token,
+ element.getAttribute('data-action'),
+ element.getAttribute('data-sitekey')
+ );
+ })
+ .then(res => res.json())
+ .then(res => {
+ if (!res) {
+ addMessage('Error: request without data');
+ return;
+ }
+
+ if (!res.error && res.data) {
+ document.getElementById('scoreButton').innerText = res.data.score;
+ return;
+ }
+
+ if (res.error) {
+ addMessage(`Server error: ${res.error}`);
+ }
+ })
+ .catch(res => {
+ addMessage(`Server error: ${res}`);
+ });
+ });
+}
+
+const verifyCallback = function (token) {
+ const recaptchaDiv = document.getElementById('recaptcha_render_div');
+
+ createAssessmentRequest(
+ token,
+ recaptchaDiv.getAttribute('data-action'),
+ recaptchaDiv.getAttribute('data-sitekey')
+ )
+ .then(res => res.json())
+ .then(res => {
+ if (!res) {
+ addMessage('Error: request without data');
+ return;
+ }
+
+ if (!res.error && res.data) {
+ document.getElementById('scoreButton').innerText = res.data.score;
+ return;
+ }
+
+ if (res.error) {
+ addMessage(`Server error: ${res.error}`);
+ }
+ })
+ .catch(res => {
+ addMessage(`Server error: ${res}`);
+ });
+};
+
+const onloadCallback = function (event) {
+ event.preventDefault();
+ document.getElementById('submitBtn').style.display = 'none';
+ const recaptcha_render_div = document.getElementById('recaptcha_render_div');
+ recaptcha_render_div.style.display = 'block';
+ grecaptcha.enterprise.render(recaptcha_render_div, {
+ sitekey: recaptcha_render_div.getAttribute('data-sitekey'),
+ callback: verifyCallback,
+ theme: 'light',
+ });
+};
+
+function addMessage(message) {
+ const myDiv = document.createElement('div');
+ myDiv.id = 'div_id';
+ myDiv.innerHTML = message + '';
+ document.body.appendChild(myDiv);
+}
diff --git a/recaptcha/demosite/app/static/home.css b/recaptcha/demosite/app/static/home.css
new file mode 100644
index 0000000000..25f46e9096
--- /dev/null
+++ b/recaptcha/demosite/app/static/home.css
@@ -0,0 +1,52 @@
+body{
+ height: 800px;
+ display: flow;
+ align-items: center;
+ font-family: "Google Sans", fantasy;
+}
+
+#scoreButton{
+ background: #f3f2f2;
+ width: 20%;
+ text-transform: uppercase;
+ font-family: "Google Sans", fantasy;
+ font-size: 1em;
+ height: 30px;
+}
+
+#user-img{
+ margin: auto;
+ position: relative;
+ top: -35px;
+ max-width: 200px;
+ max-height: 100px;
+}
+
+#submitBtn{
+ background: #4285f4;
+ width: 35%;
+ font-family: "Google Sans", fantasy;
+ font-weight: bold;
+ font-size: 1.25em;
+ height: 55px;
+}
+
+a{
+ color: #4285f4 !important;
+}
+
+.card-panel{
+ text-align: center;
+}
+
+.card-panel h3{
+
+ font-family: "Google Sans", fantasy;
+ font-weight: 200 !important;
+ margin-top: -1em;
+
+}
+
+#recaptcha_render_div{
+ display: none;
+}
diff --git a/recaptcha/demosite/app/static/recaptcha.png b/recaptcha/demosite/app/static/recaptcha.png
new file mode 100644
index 0000000000..d4210b98f6
Binary files /dev/null and b/recaptcha/demosite/app/static/recaptcha.png differ
diff --git a/recaptcha/demosite/app/templates/login.html b/recaptcha/demosite/app/templates/login.html
new file mode 100644
index 0000000000..1c41145fa9
--- /dev/null
+++ b/recaptcha/demosite/app/templates/login.html
@@ -0,0 +1,60 @@
+
+
+
+
+ reCAPTCHA-Enterprise
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/recaptcha/demosite/app/templates/signup.html b/recaptcha/demosite/app/templates/signup.html
new file mode 100644
index 0000000000..ba8d7823b7
--- /dev/null
+++ b/recaptcha/demosite/app/templates/signup.html
@@ -0,0 +1,77 @@
+
+
+
+
+ reCAPTCHA-Enterprise
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+