From 460dcf7946cd2c6d138dc75d0d7535161dec8ac2 Mon Sep 17 00:00:00 2001
From: "Marco Emilio \"sphakka\" Poleggi" <marcoep@ieee.org>
Date: Fri, 17 Jan 2025 15:07:14 +0100
Subject: [PATCH] Frontend: fix for CORS with dynmaic host probing. K8s:
 backend uses NodePort. More documentation

Signed-off-by: Marco Emilio "sphakka" Poleggi <marcoep@ieee.org>
---
 Ansible/hosts.yml                             |  9 +-
 .../playbooks/files/backend-deployment.yaml   |  5 +-
 Application/frontend/views/dashboard.html     | 82 +++++++++++++------
 Application/frontend/views/login.html         | 61 ++++++++------
 Application/frontend/views/signup.html        | 52 ++++++------
 README.md                                     | 35 +++++++-
 6 files changed, 156 insertions(+), 88 deletions(-)

diff --git a/Ansible/hosts.yml b/Ansible/hosts.yml
index ad77f8d..fddec52 100644
--- a/Ansible/hosts.yml
+++ b/Ansible/hosts.yml
@@ -1,11 +1,4 @@
 all:
   hosts:
-    testserver:
-      ansible_ssh_host: '<your-VM-IP>'
-
-    # aliases
-    tfserver:
-      testserver:
-
     project-web-sso:
-      testserver:
+      ansible_ssh_host: '<your-VM-IP>'
diff --git a/Ansible/playbooks/files/backend-deployment.yaml b/Ansible/playbooks/files/backend-deployment.yaml
index cf6a0b6..52b794e 100644
--- a/Ansible/playbooks/files/backend-deployment.yaml
+++ b/Ansible/playbooks/files/backend-deployment.yaml
@@ -20,7 +20,7 @@ spec:
     spec:
       containers:
       - name: backend
-        image: web-sso-backend:latest
+        image: <your-Docker-Hub-username>/web-sso-backend:latest
         ports:
         - containerPort: 8000
         # @TODO: declare env from config map
@@ -37,10 +37,11 @@ kind: Service
 metadata:
   name: backend-service
 spec:
+  type: NodePort
   selector:
     app: web-sso
     tier: backend
   ports:
   - port: 8000
     targetPort: 8000
-  type: ClusterIP
+    nodePort: 30080
diff --git a/Application/frontend/views/dashboard.html b/Application/frontend/views/dashboard.html
index 5a231c8..df74b4e 100644
--- a/Application/frontend/views/dashboard.html
+++ b/Application/frontend/views/dashboard.html
@@ -20,6 +20,20 @@
              background-color: white;
              border-radius: 8px;
              box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+         }
+		 .form-group {
+             margin-bottom: 15px;
+         }
+		 input {
+             width: 100%;
+             padding: 8px;
+             border: 1px solid #ddd;
+             border-radius: 4px;
+             box-sizing: border-box;
+         }
+         label {
+             display: block;
+             margin-bottom: 5px;
          }
          .button {
              display: inline-block;
@@ -44,19 +58,29 @@
 	<body>
 		<div class="container">
 			<h1 class="welcome-text">Welcome to Dashboard</h1>
-			<p>You are successfully logged in!</p>
+			<p>You are logged in!</p>
 			<button onclick="logout()" class="button">Logout</button>
-			<button onclick="unenroll()" class="button">Remove account</button>
+
+			<form id="unenrollForm">
+                <div class="form-group">
+                    <label for="password">Password:</label>
+                    <input type="password" id="password" name="password">
+                </div>
+                <button type="submit" class="button">Remove account</button>
+            </form>
 		</div>
 
 		<script>
 		 /* Get email from URL or localStorage (you should implement proper state
 			management) */
 		 const email = localStorage.getItem('userEmail');
-         console.log(email);
+         /* The backend is supposed to run in the same host */
+		 const backendHost = window.location.hostname;
+
          async function logout() {
              try {
-                 const response = await fetch('http://localhost:8000/logout', {
+                 const response = await fetch(
+					 'http://' + backendHost + ':8000/logout', {
                      method: 'POST',
                      headers: {
                          'Content-Type': 'application/json',
@@ -76,28 +100,36 @@
                  alert('Error during logout. ' + error);
              }
          }
-		 async function unenroll() {
-             try {
-                 const response = await fetch('http://localhost:8000/unenroll', {
-                     method: 'POST',
-                     headers: {
-                         'Content-Type': 'application/json',
-                     },
-                     body: JSON.stringify({ "email": email }),
-                 });
-                 const data = await response.json();
 
-                 if (data.status === 'OK:UNENROLLED') {
-					 alert('You have been unsubscribed.');
-                     localStorage.removeItem('userEmail');
-                     window.location.href = 'index.html';
-                 } else {
-                     alert('Unenroll failed: ' + data.status);
-                 }
-             } catch (error) {
-                 alert('Error during unenroll. ' + error);
-             }
-         }
+		 document.getElementById('unenrollForm').addEventListener(
+			 'submit', async (e) => {
+				 e.preventDefault();
+				 const password = document.getElementById('password').value;
+
+				 try {
+					 const response = await fetch(
+						 'http://' + backendHost + ':8000/unenroll', {
+							 method: 'POST',
+							 headers: {
+								 'Content-Type': 'application/json',
+							 },
+							 body: JSON.stringify({ email, password }),
+						 }
+					 );
+					 const data = await response.json();
+
+					 if (data.status === 'OK:UNENROLLED') {
+						 alert('You have been unsubscribed.');
+						 localStorage.removeItem('userEmail');
+						 window.location.href = 'index.html';
+					 } else {
+						 alert('Unenroll failed: ' + data.status);
+					 }
+				 } catch (error) {
+					 alert('Error during unenroll. ' + error);
+				 }
+			 }
+		 );
 		</script>
 	</body>
 </html>
diff --git a/Application/frontend/views/login.html b/Application/frontend/views/login.html
index e9f6d8c..54386ce 100644
--- a/Application/frontend/views/login.html
+++ b/Application/frontend/views/login.html
@@ -65,7 +65,7 @@
                 </div>
                 <div class="form-group">
                     <label for="password">Password:</label>
-                    <input type="password" id="password" name="password" required>
+                    <input type="password" id="password" name="password">
                 </div>
                 <button type="submit" class="button">Login</button>
             </form>
@@ -73,34 +73,41 @@
         </div>
 
         <script>
-         document.getElementById('loginForm').addEventListener('submit', async (e) => {
-             e.preventDefault();
-             const email = document.getElementById('email').value;
-             const password = document.getElementById('password').value;
+         document.getElementById('loginForm').addEventListener(
+			 'submit', async (e) => {
+				 e.preventDefault();
+				 const email = document.getElementById('email').value;
+				 const password = document.getElementById('password').value;
+				 /* The backend is supposed to run in the same host */
+				 const backendHost = window.location.hostname;
 
-             try {
-                 const response = await fetch('http://localhost:8000/login', {
-                     method: 'POST',
-                     headers: {
-                         'Content-Type': 'application/json',
-                     },
-                     body: JSON.stringify({ email, password }),
-                 });
-                 const data = await response.json();
+				 try {
+					 const response = await fetch(
+						 'http://' + backendHost + ':8000/login', {
+							 method: 'POST',
+							 headers: {
+								 'Content-Type': 'application/json',
+							 },
+							 body: JSON.stringify({ email, password }),
+						 }
+					 );
+					 const data = await response.json();
 
-                 if (data.status === 'OK:LOGGED_IN') {
-                     localStorage.setItem('userEmail', email);
-                     window.location.href = 'dashboard.html';
-                 } else if (data.status === 'OK:NEED_PASSWORD') {
-					 localStorage.setItem('userEmail', email);
-					 alert('Please provide your password');
-				 } else {
-                     alert('Login failed: ' + data.status);
-                 }
-             } catch (error) {
-                 alert('Error during login. ' + error);
-             }
-         });
+					 if (data.status === 'OK:LOGGED_IN' ||
+						 data.status === 'OK:SESSION_EXISTS') {
+						 localStorage.setItem('userEmail', email);
+						 window.location.href = 'dashboard.html';
+					 } else if (data.status === 'OK:NEED_PASSWORD') {
+						 localStorage.setItem('userEmail', email);
+						 alert('Please provide your password');
+					 } else {
+						 alert('Login failed: ' + data.status);
+					 }
+				 } catch (error) {
+					 alert('Error during login. ' + error);
+				 }
+			 }
+		 );
         </script>
     </body>
 </html>
diff --git a/Application/frontend/views/signup.html b/Application/frontend/views/signup.html
index c6eb4c4..48d0114 100644
--- a/Application/frontend/views/signup.html
+++ b/Application/frontend/views/signup.html
@@ -73,31 +73,37 @@
         </div>
 
         <script>
-         document.getElementById('signupForm').addEventListener('submit', async (e) => {
-             e.preventDefault();
-             const email = document.getElementById('email').value;
-             const password = document.getElementById('password').value;
+         document.getElementById('signupForm').addEventListener(
+			 'submit', async (e) => {
+				 e.preventDefault();
+				 const email = document.getElementById('email').value;
+				 const password = document.getElementById('password').value;
+				 /* The backend is supposed to run in the same host */
+				 const backendHost = window.location.hostname;
 
-             try {
-                 const response = await fetch('http://localhost:8000/enroll', {
-                     method: 'POST',
-                     headers: {
-                         'Content-Type': 'application/json',
-                     },
-                     body: JSON.stringify({ email, password }),
-                 });
-                 const data = await response.json();
+				 try {
+					 const response = await fetch(
+						 'http://' + backendHost + ':8000/enroll', {
+							 method: 'POST',
+							 headers: {
+								 'Content-Type': 'application/json',
+							 },
+							 body: JSON.stringify({ email, password }),
+						 }
+					 );
+					 const data = await response.json();
 
-                 if (data.status === 'OK:ENROLLED') {
-                     alert('Sign up successful! Please login.');
-                     window.location.href = 'login.html';
-                 } else {
-                     alert('Sign up failed: ' + data.status);
-                 }
-             } catch (error) {
-                 alert('Error during sign up. ' + error);
-             }
-         });
+					 if (data.status === 'OK:ENROLLED') {
+						 alert('Sign up successful! Please login.');
+						 window.location.href = 'login.html';
+					 } else {
+						 alert('Sign up failed: ' + data.status);
+					 }
+				 } catch (error) {
+					 alert('Error during sign up. ' + error);
+				 }
+			 }
+		 );
         </script>
     </body>
 </html>
diff --git a/README.md b/README.md
index a1b60f0..244f535 100644
--- a/README.md
+++ b/README.md
@@ -357,8 +357,12 @@ map:
    version you developed in
    [Lab-Ansible](https://gitedu.hesge.ch/lsds/teaching/bachelor/cloud-and-deployment/lab-ansible)
    Task #10, to (commit all related files in directory `Ansible/`):
-    - expose the application portal's IP (i.e, the load-balancer's) to the
-      Internet via `socat` or other mechanism of your choice;
+    - expose the following IPs to the Internet via `socat` or other
+      mechanism of your choice;
+        + the application portal's IP (i.e, the load-balancer's). Port
+          forwarding: 3000 (internal) -> 80 (external);
+        + the backend node's IP. Port forwarding: 30080 (internal *NodePort*)
+          -> 8000 (external);
     - rebuild and push the application images to your Docker Hub
       repository. These shall be [`local_action`
       tasks.](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_delegation.html)
@@ -490,6 +494,11 @@ $ make -s docker-push
 You can manage your containers with the other make commands: dstop, dstart,
 drm, etc.
 
+:alert: To avoid [CORS-related
+errors](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors) when
+connecting to the application portal, make sure to point your browser to the URL
+'http://localhost:3000'.
+
 
 #### Kubernetes
 
@@ -511,6 +520,27 @@ Of course, once all files are ready, any related operations shall be handled
 by Ansible. Specifically, a change in the ConfigMap shall trigger a back-end
 service redeployment.
 
+:bulb: You may notice that the backend deployment service is of type
+'NodePort': that allows the KinD worker node (IP like '172.x.x.x') to expose a
+port (by default '30080') that shall be forwarded (e.g., via `socat`) to the
+port expected by the frontend ('8000'). :alert: **Without the port forwarding,
+the frontend will not be able to connect to the backend!**
+
+Example. After deploying the backend, get the worker node's IP:
+``` shell
+VM$ kubectl get node/kind-worker -o jsonpath='{.status.addresses[0].address}'
+172.18.0.3
+```
+Then get the backend service's NodePort:
+``` shell
+VM$ kubectl get service/backend-service -o jsonpath='{.spec.ports[0].nodePort}'
+30080
+```
+Finally, expose the backend IP with port forwarding:
+``` shell
+VM$ nohup sudo socat -ly tcp-listen:8000,reuseaddr,fork tcp:172.18.0.3:30080 &
+```
+
 :bulb: For better security, you may use a K8s Secret manifest instead of the
 ConfigMap. This would be a **bonus**.
 
@@ -552,4 +582,3 @@ The following tests shall be passed by your implementation:
       9. Logout an enrolled user:
           * with an active session: shall succeed
           * without an active session: shall fail
-    
-- 
GitLab