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