Add Playwright tests for live site authentication and race page loading

- Introduced `auth.setup.js` to handle authentication against the live site and store the session state.
- Created `live-race.spec.js` to test loading a live race page with an authenticated session, including cookie validation.
- Added utility functions in `live-site-test-utils.js` for managing authentication, dismissing cookie banners, and checking UI states.
- Included a temporary JSON file for live state inspection.
- Updated deployment manifest to reflect new and modified files.
- Implemented `_inc_faceai_identity.jsp` for managing FaceAI identity cookies and included it in relevant JSP files.
- Added language management JavaScript in `lang.js`.
- Adjusted `fotoCR-en.jsp` and `fotoCR.jsp` to include the FaceAI identity logic.
- Created a tarball for staging deployment.
This commit is contained in:
MaddoScientisto 2026-04-19 10:26:34 +02:00
commit 1d1bccdae6
23 changed files with 4358 additions and 11 deletions

View file

@ -102,6 +102,7 @@ code example here
- **Include Examples**: Real code snippets are more effective than descriptions
- **Stay Current**: Reference current versions and best practices
- **Link Resources**: Include official documentation and authoritative sources
- **Capture validated shell quirks**: For environment-specific instruction files, record proven terminal behaviors, quoting pitfalls, failing command patterns, and the known-good command form that replaced them
### Instruction Altitude (Goldilocks Zone)

View file

@ -35,6 +35,12 @@ If you need a single elevated command:
ssh -tt -i C:\Users\Maddo\.ssh\id_rsa -p 410 marco@83.149.164.4 "sudo tcsh -c 'command here'"
```
From PowerShell on Windows, prefer invoking the SSH binary directly instead of wrapping it in `cmd /c`:
```powershell
& 'C:\Windows\System32\OpenSSH\ssh.exe' -tt -i 'C:\Users\Maddo\.ssh\id_rsa' -p 410 'marco@83.149.164.4'
```
## Shell Behavior On This Host
- The remote login shell behaves as `tcsh`.
@ -42,11 +48,15 @@ ssh -tt -i C:\Users\Maddo\.ssh\id_rsa -p 410 marco@83.149.164.4 "sudo tcsh -c 'c
- The server `sh` does not support `-l`, so use `sh -c`, not `sh -lc`.
- `tcsh` treats redirection and pipelines differently from POSIX shells; commands like `find ... 2>/dev/null | head` can fail with `Ambiguous output redirect` unless the whole payload runs under `sh -c`.
- Prefer one remote command per SSH invocation when doing reconnaissance. Complex commands with pipes, grouped expressions, or escaped parentheses are much more likely to break under PowerShell-to-SSH-to-`tcsh` quoting.
- On Windows PowerShell, avoid `cmd /c "ssh ..."` and `cmd /c "scp ..."` wrappers for anything nontrivial. Nested quoting can collapse before SSH runs and spill later tokens into the local PowerShell session, which leads to misleading local errors such as `sudo: The term 'sudo' is not recognized` or local attempts to run `cksum`.
- Prefer the PowerShell call operator form `& 'C:\Windows\System32\OpenSSH\ssh.exe' ...` and pass the remote command as a single argument when you must stay non-interactive.
- If PowerShell shows the continuation prompt `? >`, the command was malformed locally before SSH executed it. Cancel it and rerun a simpler command instead of trying to answer the prompt.
- If `sudo` reports that a terminal is required, reconnect with `-tt`.
- When running remote commands from PowerShell, quoting can break if the command contains both nested quotes and file paths with spaces.
- For read-only verification commands from PowerShell, prefer `ssh ... --% <remote command>` so the remote command is passed verbatim.
- For `promote-file.sh` calls that target paths with spaces, prefer a local PowerShell loop that passes the full remote command as a single SSH argument instead of building one long nested quoted command.
- For multi-step privileged work, prefer opening one interactive SSH session, then running `sudo tcsh`, then issuing commands sequentially inside that shell. This is more reliable than trying to encode several `sudo tcsh -c 'a ; b ; c'` operations through PowerShell quoting.
- In an interactive `tcsh` root shell, do not re-send a password or any other text starting with `!` after the password prompt has already succeeded. `tcsh` interprets `!` as history expansion and will emit `Event not found`.
- If repeated SSH commands start cancelling or interleaving poorly in the same terminal, rerun them sequentially instead of in parallel.
## Mail Template Runtime Notes
@ -88,6 +98,14 @@ ssh -tt -i C:\Users\Maddo\.ssh\id_rsa -p 410 marco@83.149.164.4 "sudo tcsh -c 'c
- Incoming staging root: `/home/marco/regalamiunsorriso/incoming/www`
- Live site root: `/home/sites/regalamiunsorriso/www`
## Tomcat Logs And Runtime Clues
- The active Tomcat installation on this host is under `/usr/local/apache-tomcat-9.0`.
- The most useful live runtime log is `/usr/local/apache-tomcat-9.0/logs/catalina.out`.
- Rotated Tomcat logs are under `/usr/local/apache-tomcat-9.0/logs/`, including files such as `catalina.YYYY-MM-DD.log` and `localhost.YYYY-MM-DD.log`.
- Access to generated JSP work files under `/usr/local/apache-tomcat-9.0/work` may require root.
- A broken JSP on this host can still return `HTTP 200` with a visibly truncated HTML body instead of a clean 500 response; when that happens, fetch part of the page body with `curl -L <url> | head -n ...` and compare the cutoff point with recent `catalina.out` output.
## Staging Workflow
When `www/**` files need deployment:
@ -103,6 +121,9 @@ Example staging command pattern:
tar -cf - -C K:\various\regalamiunsorriso <file-list-under-www> | ssh -i C:\Users\Maddo\.ssh\id_rsa -p 410 marco@83.149.164.4 "tar -xf - -C /home/marco/regalamiunsorriso/incoming"
```
- The streamed tar extraction into `/home/marco/regalamiunsorriso/incoming` works as the unprivileged `marco` user and avoids the permission problems seen when uploading an archive and trying to unpack it with `sudo tar`.
- Do not rely on `sudo tar` for staging on this host. `marco` is not permitted to run that extraction as root.
## Promotion Rules
- Promotion to the live site must happen through `sudo tcsh`.
@ -136,6 +157,13 @@ $remote = "sudo tcsh -c \"/home/marco/promote-file.sh '<staged-path>' '<live-pat
& ssh -tt -i 'C:\Users\Maddo\.ssh\id_rsa' -p 410 'marco@83.149.164.4' $remote
```
If the deployment needs more than one privileged action or may prompt for a password, prefer this sequence instead of packing everything into one quoted SSH command:
1. Open an interactive SSH session with `-tt`.
2. Run `sudo tcsh`.
3. Run `/home/marco/promote-file.sh ...` commands one at a time.
4. Run `ls -l`, `stat -f`, and `cksum` in that same root shell.
Behavior of `promote-file.sh`:
- If the destination already exists, it copies the file and restores that destination file's original owner, group, and mode.

View file

@ -14,3 +14,8 @@ FACEAI_UPLOAD_ROOT=/data/runtime/uploads
FACEAI_LOG_ROOT=/data/logs
FACEAI_PKL_ROOT=/data/pkl
FACEAI_MATCHER_BINARY=/opt/face-recognition/face_matcher
LIVE_SITE_BASE_URL=https://www.regalamiunsorriso.it
LIVE_SITE_LOGIN_URL=https://www.regalamiunsorriso.it/login_clienti-it.html
LIVE_SITE_RACE_URL=https://www.regalamiunsorriso.it/42%20HALF%20MARATHON%20FIRENZE_gara-1018545---96-1.html
LIVE_SITE_USERNAME=
LIVE_SITE_PASSWORD=

1
faceai/.gitignore vendored
View file

@ -3,3 +3,4 @@ apps/frontend/dist/
.env
playwright-report/
test-results/
tests/live-site/.auth/

View file

@ -172,6 +172,35 @@ If you want to keep the local containers running after the test for manual inspe
FACEAI_E2E_KEEP_STACK=1
```
## Live Site Playwright Checks
The `faceai/` workspace now also includes a separate Playwright project for the live site. It is isolated from the Docker-backed simulator suite and is intended to verify that production login still works and that a real race page loads correctly after authentication.
Set these environment variables before running it:
```bash
LIVE_SITE_BASE_URL=https://www.regalamiunsorriso.it
LIVE_SITE_LOGIN_URL=https://www.regalamiunsorriso.it/login_clienti-it.html
LIVE_SITE_RACE_URL=https://www.regalamiunsorriso.it/42%20HALF%20MARATHON%20FIRENZE_gara-1018545---96-1.html
LIVE_SITE_USERNAME=your-login
LIVE_SITE_PASSWORD=your-password
```
Then run:
```bash
npm run test:live:install
npm run test:live
```
What it does:
- opens the live login page
- signs in with the supplied credentials
- persists authenticated Playwright storage state under `tests/live-site/.auth/user.json`
- opens the configured live race URL
- verifies the account UI is present and the race search form renders correctly
## Optional Backend And Frontend Dev Loop
If you only want to iterate on the app without the PHP simulator, you can still run the public site and the processor separately. The queue-backed flow now requires Redis and the processor, so `npm run dev` alone is no longer the full stack.

1116
faceai/anonymous_output.html Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

0
faceai/headers.txt Normal file
View file

359
faceai/login_response.html Normal file
View file

@ -0,0 +1,359 @@
<!DOCTYPE html>
<html lang="en"><!-- InstanceBegin template="/Templates/rus.dwt" codeOutsideHTMLIsLocked="false" -->
<head>
<!-- _inc_lang.jsp -->
<script>var webApp='';</script>
<script src="_js/lang.js"></script>
<input type="hidden" name="lang" id="lang" value="en" />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<!-- [favicon] begin -->
<link rel="shortcut icon" type="image/x-icon" href="images/favicon.ico"/>
<link rel="icon" type="image/x-icon" href="images/favicon.ico" />
<!-- [favicon] end -->
<!-- JSP -->
<!-- InstanceBeginEditable name="Bean" -->
<!-- InstanceEndEditable -->
<!-- InstanceBeginEditable name="doctitle" -->
<title>Regalami Un Sorriso ETS - User Area</title>
<!-- InstanceEndEditable -->
<!-- Bootstrap core CSS -->
<link href="vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="css/font-awesome.min.css">
<!-- Roboto Font -->
<link href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="css/custom-style.css" rel="stylesheet">
<!-- Css Datepicker -->
<link href="addons/datepicker/css/bootstrap-datepicker.standalone.min.css" rel="stylesheet">
<link rel="stylesheet" href='admin/_V4/_css/ajaxLoading.css'>
<!-- InstanceBeginEditable name="head" -->
<!-- InstanceEndEditable -->
<!-- _inc_cookie.jsp -->
<!-- Begin Cookie Consent plugin by Silktide - http://silktide.com/cookieconsent -->
<script type="text/javascript">
window.cookieconsent_options = {
"message": "This website or the third-party tools used here make use of cookies necessary for its operation and useful for the purposes described in the cookie policy. To learn more or refuse consent to all or some cookies, please refer to the cookie policy. By closing this banner, scrolling this page, clicking on a link, or continuing to browse otherwise, you consent to the use of cookies.",
"dismiss": "Accept",
"learnMore": "Cookie Policy",
"link": "https://www.regalamiunsorriso.it/privacy-en.html",
"theme": "dark-bottom"
};
</script>
<script type="text/javascript" src="js/cookieconsent.min.js"></script>
<!-- End Cookie Consent plugin -->
<!-- _inc_head.jsp -->
<!-- Matomo Tag Manager -->
<script>
var _mtm = window._mtm = window._mtm || [];
_mtm.push({'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'});
(function() {
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src='https://matomo.acxent.it/js/container_TKh2j1eF.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Tag Manager -->
</head>
<body>
<!-- Page Content -->
<!-- InstanceBeginEditable name="main" -->
<input name="id_tipoSel" type="hidden" id="id_tipoSel" value="0">
<!-- facebook init script-->
<div id="fb-root"></div>
<script>
window.fbAsyncInit = function() {
FB.init({
appId : '5858950737524161',
autoLogAppEvents : true,
xfbml : true,
version : 'v15.0',
});
};
</script>
<script async defer crossorigin="anonymous" src="https://connect.facebook.net/it_IT/sdk.js"></script>
<!-- Bootstrap core JavaScript -->
<script src="vendor/jquery/jquery.min.js"></script>
<script src="vendor/popper/popper.min.js"></script>
<script src="admin/_V4/_js/_acxent.js"></script>
<script src="_js/rus-ecom-240621.js"></script>
<script src="addons/datepicker/js/bootstrap-datepicker.min.js"></script>
<script src="addons/datepicker/locales/bootstrap-datepicker.it.min.js"></script>
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
<!-- google sign in -->
<script src="https://accounts.google.com/gsi/client" async defer></script>
<!-- -->
<!-- owl -->
<link rel="stylesheet" href="vendor/owl-carousel/owl.carousel.min.css">
<link rel="stylesheet" href="vendor/owl-carousel/owl.theme.default.min.css">
<!-- sweet alert -->
<link rel="stylesheet" type="text/css" href="js/sweetalert/sweetalert.css">
<script src="js/sweetalert/sweetalert.min.js"></script>
<a id="top"></a>
<!-- Navigation -->
<nav class="navbar fixed-top navbar-expand-lg navbar-light bg-white fixed-top">
<div class="container"> <a class="navbar-brand" href="index.html"><img src="images/layout/regalami-un-sorriso-ets-640.png" alt="Regalami_Un_Sorriso_Ets" width="100"></a>
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav">
<li class="nav-item"> <a class="nav-link " href="index.html">Home</a> </li>
<li class="nav-item"> <a class="nav-link " href="associazione-en.html">Association</a> </li>
<li class="nav-item"> <a class="nav-link " href=" rivista+atleticaImmagine-en.html"><img src="pics/atletica2.jpg" class="img-fluid" style="max-width: 110px"></a> </li>
<li class="nav-item dropdown"> <a href="#" class="dropdown-toggle nav-link " data-toggle="dropdown">Photo <b class="caret"></b></a>
<ul class="dropdown-menu multi-column columns-2" style="overflow-y:scroll">
<div class="row">
<div class="col-sm-6 vertical-divider">
<h2>EVENT PHOTOS</h2>
<ul class="multi-column-dropdown">
<li><a href="Carnevale-elenco_eventi-14---1-en.html">Carnival</a></li>
<li><a href="Defibrillatori-elenco_eventi-20---1-en.html">Defibrillators</a></li>
<li><a href="Feste-elenco_eventi-12---1-en.html">Parties</a></li>
<li><a href="Lions-elenco_eventi-4---1-en.html">Lions</a></li>
<li><a href="Manifestazioni-elenco_eventi-13---1-en.html">Demonstrations</a></li>
<li><a href="Panathlon-elenco_eventi-3---1-en.html">Panathlon</a></li>
<li><a href="Viaggi-elenco_eventi-18---1-en.html">Trips</a></li>
<li><a href="Volantini storici-elenco_eventi-21---1-en.html">Historical flyers</a></li>
</ul>
</div>
<div class="col-sm-6 ">
<h2>SPORTS PHOTOS</h2>
<ul class="multi-column-dropdown">
<li><a href="Basket-elenco_eventi-17---1-en.html">Basketball</a></li>
<li><a href="Ciclismo-elenco_eventi-6---1-en.html">Bike</a></li>
<li><a href="Golf-elenco_eventi-15---1-en.html">Golf</a></li>
<li><a href="Multidiscipline-elenco_eventi-11---1-en.html">Multidiscipline</a></li>
<li><a href="Podismo-elenco_eventi-5---1-en.html">Running</a></li>
</ul>
</div>
</div>
</ul>
</li>
<li class="nav-item dropdown show"> <a class="nav-link btn btn-sm btn-warning dropdown-toggle" data-toggle="dropdown" href="#" aria-expanded="true">Archive <b class="caret"></b></a>
<ul class="dropdown-menu ">
<li>
<h2 style="margin: 0px;font-size: 12px;"><a style="padding-left: 0px;" href="gallery1.php">.Fino al 2011.</a></h2>
</li>
<li>
<h2 style="margin: 0px;font-size: 12px;"><a style="padding-left: 0px;" href="gallery2.php">2012-2017</a></h2>
</li>
</ul>
</li>
<a href="dettaglio_clienti-en.html"><img src="https://www.regalamiunsorriso.it/images/btn_donateCC_LG.gif" border="0" name="submit" alt="PayPal - The easiest and safest online payment system!"></a>
<!--
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
<input type="hidden" name="cmd" value="_s-xclick">
<input type="hidden" name="hosted_button_id" value="4089564">
<input type="image" src="https://www.regalamiunsorriso.it/images/btn_donateCC_LG.gif" border="0" name="submit" alt="PayPal - Il sistema di pagamento online piu' facile e sicuro!">
</form>-->
</ul>
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown"> <a class="nav-link dropdown-toggle active" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <i class="fa fa-user" aria-hidden="true"></i> My account </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> <a class="dropdown-item" href="dettaglio_clienti-en.html"><i class="fa fa-user" aria-hidden="true"></i> Account Detail</a> <a class="dropdown-item" href="586291449-user_logout-en.html"><i class="fa fa-sign-out" aria-hidden="true"></i> Logout</a> </div>
</li>
<li class="nav-item"> <a class="nav-link" href="https://it-it.facebook.com/pg/Regalami-un-sorriso-ETS-189377806523/community/"><img src="images/FB-f-Logo__blue_29.png" class="img-fluid" alt="Facebook"></a></li>
</ul>
<ul class="navbar-nav ml-auto" data="langMenu">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="langDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <img src="images/flags/en.png" alt="English" title="English" class="flag"> </a>
<div class="dropdown-menu" aria-labelledby="langDropdownMenuLink">
<a class="dropdown-item" href="index-it.html"> <img src="images/flags/it.png" class="flag" alt="Italiano" title="Italiano"> Italiano </a>
</div>
</ul>
</div>
</div>
</nav>
<div class="container my-3">
<div class="row">
<div class="col-lg-6 my-4">
<h1 class="my-3">User Area</h1>
<div class="col-lg-12 my-12"> <a href="index.html" class="btn btn-primary btn-block"><br>
View photos<br>
<br>
</a> </div>
<p class="mt-5 mb-4">HI <strong>Giacomelli Piero</strong><br>
Address: <strong>Fosso del Masi n. 24, Prato 59100 (PO)</strong><br>
Company belonging to: <strong>REGALAMI UN SORRISO ONLUS </strong><br>
Telephone: <strong>3487258208</strong><br>
E-mail: <strong><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="5828313d2a371828313d2a373f31393b37353d343431763b3735">[email&#160;protected]</a></strong><br>
Tax ID code: <strong>GCMPRI58H25G999U</strong><br>
Expiration Date: <strong>15/06/2030</strong><br>
No. of photos displayed/No. of photos max: <strong>37045/0</strong><br>
Number of photos viewed today: <strong>0</strong></p>
<p class="">Please verify that the information is correct. For changes, please write to <a href="/cdn-cgi/l/email-protection#74121b001b34041d11061b131d15171b191118181d5a171b19"><span class="__cf_email__" data-cfemail="95f3fae1fad5e5fcf0e7faf2fcf4f6faf8f0f9f9fcbbf6faf8">[email&#160;protected]</span></a> or go to <a href="registrazione_utente.html">Edit Data</a> <br>
Please note that tacit consent to the accuracy of the member personal data is required.</p>
<p class="">.Mancano. <strong>1518 </strong> .giorni alla scadenza..<br>
</p>
</div>
<div class="col-lg-6 my-4" ">
</div>
<div class="col-md-12 text-danger font-weight-bold"> Login ok </div>
</div>
</div>
<!-- /.container --> <!-- InstanceEndEditable -->
<!-- Footer -->
<!-- inc_footer.jsp -->
<!-- sweet alert -->
<link rel="stylesheet" type="text/css" href="js/sweetalert/sweetalert.css">
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js" defer></script><script src="js/sweetalert/sweetalert.min.js"></script>
<footer>
<div class="container py-3">
<div class="row">
<div class="col-lg-3 col-md-6">
<h4>Contacts</h4>
<p>ETS Regalami un sorriso<br>
Via Torquato Tasso, 23/C<br>
59100 Prato PO<br>
CF 92076170486<br>
<a href="/cdn-cgi/l/email-protection#e98f869d86a999808c9b868e80888a86848c858580c78a8684"><span class="__cf_email__" data-cfemail="b8ded7ccd7f8c8d1ddcad7dfd1d9dbd7d5ddd4d4d196dbd7d5">[email&#160;protected]</span></a><br>
<a href="/cdn-cgi/l/email-protection#36445351575a575b5f4358455944445f45597646535518465f534459515f5755595b535a5a5f1855595b"><span class="__cf_email__" data-cfemail="7d0f181a1c111c101408130e120f0f140e123d0d181e530d14180f121a141c1e121018111114531e1210">[email&#160;protected]</span></a> <br>
Alone <i class="fa fa-whatsapp fa-2x" aria-hidden="true" title="WhatsApp" style="color:#25e47b"></i>: 348 7258209
</p>
</div>
<div class="col-lg-3 col-md-6">
<h4>Information</h4>
<ul class="list-unstyled">
<li><a href="associazione-en.html">Association</a></li>
<li><a href="privacy-en.html">Privacy</a></li>
<li><a href="cookies-en.html">Cookies</a></li>
</ul>
</div>
</div>
</div>
<hr>
<p class="text-center text-secondary"><small>Copyright</small></p>
<p class="triangle"><a href="#top"><i class="fa fa-arrow-up" aria-hidden="true"></i></a></p>
</footer>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js" defer></script><script src="_js/noTastoDx.js"></script>
<script>
$('#datepicker-sport').datepicker({
language: "it"
});
$('#datepicker-eventi').datepicker({
language: "it"
});
</script>
<!-- InstanceBeginEditable name="lastStuff" --> <!-- InstanceEndEditable -->
<div class="modal-loading"></div>
</body>
<!-- InstanceEnd --></html>

View file

@ -16,7 +16,10 @@
"start:processor": "npm run start --workspace @regalami/faceai-processor",
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:install": "playwright install chromium"
"test:e2e:install": "playwright install chromium",
"test:live": "playwright test -c playwright.live.config.js",
"test:live:headed": "playwright test -c playwright.live.config.js --headed",
"test:live:install": "playwright install chromium"
},
"devDependencies": {
"@playwright/test": "^1.59.1",

View file

@ -0,0 +1,38 @@
const path = require('path');
const { defineConfig } = require('@playwright/test');
const authFile = path.join(__dirname, 'tests/live-site/.auth/user.json');
module.exports = defineConfig({
testDir: './tests/live-site',
timeout: 2 * 60 * 1000,
expect: {
timeout: 20 * 1000
},
fullyParallel: false,
workers: 1,
reporter: [['list'], ['html', { open: 'never', outputFolder: 'playwright-report/live-site' }]],
outputDir: 'test-results/live-site',
use: {
baseURL: process.env.LIVE_SITE_BASE_URL || 'https://www.regalamiunsorriso.it',
browserName: 'chromium',
headless: true,
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.js/
},
{
name: 'live-chromium',
dependencies: ['setup'],
testIgnore: /.*\.setup\.js/,
use: {
storageState: authFile
}
}
]
});

47
faceai/probe.js Normal file
View file

@ -0,0 +1,47 @@
const { chromium } = require('playwright');
const fs = require('fs');
async function run() {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
try {
const loginUrl = process.env.LIVE_SITE_LOGIN_URL;
const raceUrl = process.env.LIVE_SITE_RACE_URL;
console.log('--- Step 1: Navigating to Login ---');
await page.goto(loginUrl, { waitUntil: 'load' });
console.log('--- Step 2: Filling credentials ---');
await page.type('#login', process.env.LIVE_SITE_USERNAME);
await page.type('#pwd', process.env.LIVE_SITE_PASSWORD);
console.log('--- Step 3: Clicking Accedi ---');
await page.click('button:has-text("Accedi")');
await page.waitForTimeout(5000); // Give it time to process and redirect
console.log('--- Step 4: Navigating to Race Page ---');
const response = await page.goto(raceUrl, { waitUntil: 'load' });
console.log('Race Page URL:', page.url());
console.log('HTTP Status:', response.status());
const cookies = await context.cookies();
const cookieNames = cookies.map(c => c.name);
console.log('Cookies:', cookieNames.join(', '));
console.log('rus_faceai_identity:', cookieNames.includes('rus_faceai_identity'));
console.log('JSESSIONID:', cookieNames.includes('JSESSIONID'));
const logoutCount = await page.locator('a[href*="logout"]').count();
const loginCount = await page.locator('a[href*="login"]').count();
console.log('Logout Link Count:', logoutCount);
console.log('Login Link Count:', loginCount);
} catch (err) {
console.error('Error occurred:', err.message);
} finally {
await browser.close();
}
}
run();

1100
faceai/response.html Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
const { test } = require('@playwright/test');
const {
AUTH_FILE,
ensureAuthDirectory,
performLiveLogin
} = require('./live-site-test-utils');
test('authenticate against the live site', async ({ page }) => {
ensureAuthDirectory();
await performLiveLogin(page);
await page.context().storageState({ path: AUTH_FILE });
});

View file

@ -0,0 +1,33 @@
const { test, expect } = require('@playwright/test');
const {
LIVE_SITE_RACE_URL,
dismissCookieBanner,
expectRacePageLoaded,
performLiveLogin,
waitForLoggedInUi
} = require('./live-site-test-utils');
test('loads a live race page with an authenticated session', async ({ page }) => {
await page.goto(LIVE_SITE_RACE_URL, { waitUntil: 'domcontentloaded' });
await dismissCookieBanner(page);
try {
await waitForLoggedInUi(page);
} catch (error) {
await performLiveLogin(page);
await page.goto(LIVE_SITE_RACE_URL, { waitUntil: 'domcontentloaded' });
await dismissCookieBanner(page);
await waitForLoggedInUi(page);
}
await expectRacePageLoaded(page);
await expect(page.locator('h1')).toContainText(/HALF MARATHON FIRENZE|Competitions|Gare/i);
const cookies = await page.context().cookies(LIVE_SITE_RACE_URL);
const faceAiIdentityCookie = cookies.find((cookie) => cookie.name === 'rus_faceai_identity');
expect(faceAiIdentityCookie, 'Expected the race page to mint the FaceAI identity cookie for the authenticated session.').toBeTruthy();
expect(faceAiIdentityCookie.httpOnly).toBe(true);
expect(faceAiIdentityCookie.secure).toBe(true);
expect(faceAiIdentityCookie.value).toMatch(/\./);
});

View file

@ -0,0 +1,92 @@
const fs = require('fs');
const path = require('path');
const { expect } = require('@playwright/test');
const LIVE_SITE_BASE_URL = process.env.LIVE_SITE_BASE_URL || 'https://www.regalamiunsorriso.it';
const LIVE_SITE_LOGIN_URL = process.env.LIVE_SITE_LOGIN_URL || `${LIVE_SITE_BASE_URL}/login_clienti-it.html`;
const LIVE_SITE_RACE_URL = process.env.LIVE_SITE_RACE_URL || `${LIVE_SITE_BASE_URL}/42%20HALF%20MARATHON%20FIRENZE_gara-1018545---96-1.html`;
const LIVE_SITE_USERNAME = process.env.LIVE_SITE_USERNAME || '';
const LIVE_SITE_PASSWORD = process.env.LIVE_SITE_PASSWORD || '';
const AUTH_FILE = path.join(__dirname, '.auth', 'user.json');
function ensureAuthDirectory() {
fs.mkdirSync(path.dirname(AUTH_FILE), { recursive: true });
}
function requireCredentials() {
if (!LIVE_SITE_USERNAME || !LIVE_SITE_PASSWORD) {
throw new Error('LIVE_SITE_USERNAME and LIVE_SITE_PASSWORD must be set before running the live-site Playwright suite.');
}
}
async function dismissCookieBanner(page) {
const cookieButton = page.getByRole('button', { name: /^(Accetto|Accept)$/i });
if (await cookieButton.count()) {
await cookieButton.first().click({ timeout: 5000 }).catch(() => {});
return;
}
const fallbackButton = page.locator('.cc-btn, .cc-dismiss, .cc-allow').filter({ hasText: /Accetto|Accept/i });
if (await fallbackButton.count()) {
await fallbackButton.first().click({ timeout: 5000 }).catch(() => {});
}
}
function loginSubmitLocator(page) {
return page.locator('a.btn').filter({ hasText: /Accedi|Sign in/i }).first();
}
async function performLiveLogin(page) {
requireCredentials();
await page.goto(LIVE_SITE_LOGIN_URL, { waitUntil: 'domcontentloaded' });
await dismissCookieBanner(page);
await page.locator('#login').fill(LIVE_SITE_USERNAME);
await page.locator('#pwd').fill(LIVE_SITE_PASSWORD);
await loginSubmitLocator(page).click();
await waitForLoggedInUi(page);
}
async function waitForLoggedInUi(page) {
const accountMenu = page.locator('#navbarDropdownMenuLink');
const accountLink = page.locator('a[href*="dettaglio_clienti"]');
const logoutLink = page.locator('a[href*="user_logout"]');
await expect.poll(async () => {
if (await accountMenu.count() && await accountMenu.first().isVisible().catch(() => false)) {
return 'account-menu';
}
if (await accountLink.count() && await accountLink.first().isVisible().catch(() => false)) {
return 'account-link';
}
if (await logoutLink.count() && await logoutLink.first().isVisible().catch(() => false)) {
return 'logout-link';
}
return '';
}, {
timeout: 30 * 1000,
message: 'Expected the logged-in account UI to appear after authenticating.'
}).not.toBe('');
}
async function expectRacePageLoaded(page) {
await expect(page.locator('form[onsubmit="return searching()"]')).toBeVisible();
await expect(page.locator('#id_gara')).toHaveValue(/\d+/);
await expect(page.locator('script[src*="_js/rus-ecom-240621.js"]')).toHaveCount(1);
}
module.exports = {
AUTH_FILE,
LIVE_SITE_BASE_URL,
LIVE_SITE_LOGIN_URL,
LIVE_SITE_PASSWORD,
LIVE_SITE_RACE_URL,
LIVE_SITE_USERNAME,
dismissCookieBanner,
ensureAuthDirectory,
expectRacePageLoaded,
loginSubmitLocator,
performLiveLogin,
requireCredentials,
waitForLoggedInUi
};

View file

@ -0,0 +1,45 @@
{
"cookies": [
{
"name": "JSESSIONID",
"value": "C78CCBD1771ACB52852817531958E96D",
"domain": "www.regalamiunsorriso.it",
"path": "/",
"expires": -1,
"httpOnly": true,
"secure": false,
"sameSite": "Lax"
},
{
"name": "_pk_id.6.42c1",
"value": "5710c1018f76b02c.1776585670.",
"domain": "www.regalamiunsorriso.it",
"path": "/",
"expires": 1810540870,
"httpOnly": false,
"secure": false,
"sameSite": "Lax"
},
{
"name": "_pk_ses.6.42c1",
"value": "1",
"domain": "www.regalamiunsorriso.it",
"path": "/",
"expires": 1776587471,
"httpOnly": false,
"secure": false,
"sameSite": "Lax"
},
{
"name": "g_state",
"value": "{\"i_l\":0,\"i_ll\":1776585670175,\"i_b\":\"TwramoDGp33YsAyHyO2OJCLtRobG3bzu5wR3/nYmZ0E\",\"i_e\":{\"enable_itp_optimization\":0},\"i_et\":1776585670056}",
"domain": "www.regalamiunsorriso.it",
"path": "/",
"expires": 1792137670,
"httpOnly": false,
"secure": false,
"sameSite": "Lax"
}
],
"origins": []
}

View file

@ -10,20 +10,17 @@ All files in this rollout are deployed from the current working tree.
## New Files
- None in this rollout.
- `www/_inc_faceai_identity.jsp`
- `www/_js/lang.js`
## Updated Files
- `www/faceai_config.php`
- `www/faceai_handoff.php`
- `www/faceai_return.php`
- `www/fotoCR.jsp`
- `www/fotoCR-en.jsp`
- `www/fotoCR.jsp`
## Excluded Files
- `www/faceai_simulator.php`
- `www/faceai_simulator_view.php`
- None in this rollout.
## Remote Copy Target
@ -31,7 +28,7 @@ All files in this rollout are deployed from the current working tree.
- Remote host: `marco@83.149.164.4:410`
- Remote staging path: `/home/marco/regalamiunsorriso/incoming/www`
- Remote live path: `/home/sites/regalamiunsorriso/www`
- Total files in this manifest: `5`
- Total files in this manifest: `4`
## Transfer Method

View file

@ -0,0 +1,260 @@
<%@ page language="java" import="java.nio.charset.StandardCharsets" %>
<%@ page language="java" import="java.util.Base64" %>
<%@ page language="java" import="java.lang.reflect.Method" %>
<%@ page language="java" import="javax.crypto.Mac" %>
<%@ page language="java" import="javax.crypto.spec.SecretKeySpec" %>
<%!
private String faceAiCookieEnv(String key, String defaultValue) {
String value = System.getenv(key);
if (value == null || value.trim().length() == 0) {
value = System.getProperty(key, defaultValue);
}
if (value == null) {
return defaultValue;
}
value = value.trim();
return value.length() == 0 ? defaultValue : value;
}
private String faceAiBase64Url(byte[] value) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(value);
}
private String faceAiJsonEscape(String value) {
if (value == null) {
return "";
}
StringBuilder builder = new StringBuilder(value.length() + 16);
for (int index = 0; index < value.length(); index++) {
char current = value.charAt(index);
switch (current) {
case '\\':
builder.append("\\\\");
break;
case '"':
builder.append("\\\"");
break;
case '\b':
builder.append("\\b");
break;
case '\f':
builder.append("\\f");
break;
case '\n':
builder.append("\\n");
break;
case '\r':
builder.append("\\r");
break;
case '\t':
builder.append("\\t");
break;
default:
if (current < 0x20) {
String hex = Integer.toHexString(current);
builder.append("\\u");
for (int padding = hex.length(); padding < 4; padding++) {
builder.append('0');
}
builder.append(hex);
} else {
builder.append(current);
}
break;
}
}
return builder.toString();
}
private Object faceAiInvoke(Object bean, String methodName) {
if (bean == null || methodName == null || methodName.length() == 0) {
return null;
}
try {
Method method = bean.getClass().getMethod(methodName, new Class[0]);
return method.invoke(bean, new Object[0]);
} catch (Exception ignored) {
return null;
}
}
private long faceAiUserId(Object user) {
Object value = faceAiInvoke(user, "getId_users");
if (value instanceof Number) {
return ((Number) value).longValue();
}
if (value != null) {
try {
return Long.parseLong(String.valueOf(value));
} catch (NumberFormatException ignored) {
return 0L;
}
}
return 0L;
}
private String faceAiUserString(Object user, String methodName) {
Object value = faceAiInvoke(user, methodName);
return value == null ? "" : String.valueOf(value).trim();
}
private boolean faceAiUserDaRinnovare(Object user) {
Object value = faceAiInvoke(user, "isDaRinnovare");
return value instanceof Boolean ? ((Boolean) value).booleanValue() : false;
}
private String faceAiCookieDisplayName(Object user) {
String nome = faceAiUserString(user, "getNome");
String cognome = faceAiUserString(user, "getCognome");
String displayName = (nome + " " + cognome).trim();
if (displayName.length() > 0) {
return displayName;
}
String email = faceAiUserString(user, "getEMail");
if (email.length() > 0) {
return email;
}
return String.valueOf(faceAiUserId(user));
}
private String faceAiIdentityToken(Object user, String secret, long expiresAt) throws Exception {
String email = faceAiUserString(user, "getEMail");
String membershipStatus = faceAiUserDaRinnovare(user) ? "inactive" : "active";
String payload = "{"
+ "\"type\":\"legacy-identity\","
+ "\"userId\":\"" + faceAiJsonEscape(String.valueOf(faceAiUserId(user))) + "\","
+ "\"displayName\":\"" + faceAiJsonEscape(faceAiCookieDisplayName(user)) + "\","
+ "\"email\":\"" + faceAiJsonEscape(email) + "\","
+ "\"membershipStatus\":\"" + membershipStatus + "\","
+ "\"expiresAt\":" + expiresAt
+ "}";
String body = faceAiBase64Url(payload.getBytes(StandardCharsets.UTF_8));
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = faceAiBase64Url(mac.doFinal(body.getBytes(StandardCharsets.UTF_8)));
return body + "." + signature;
}
private boolean faceAiRequestIsSecure(javax.servlet.http.HttpServletRequest request) {
if (request == null) {
return false;
}
if (request.isSecure()) {
return true;
}
String forwardedProto = request.getHeader("X-Forwarded-Proto");
if (forwardedProto != null && "https".equalsIgnoreCase(forwardedProto.trim())) {
return true;
}
String frontEndHttps = request.getHeader("Front-End-Https");
return frontEndHttps != null && "on".equalsIgnoreCase(frontEndHttps.trim());
}
private void faceAiWriteCookieHeader(javax.servlet.http.HttpServletResponse response, String cookieName, String cookieValue, int maxAgeSeconds, boolean secureCookie) {
StringBuilder headerValue = new StringBuilder();
headerValue.append(cookieName).append('=').append(cookieValue == null ? "" : cookieValue);
headerValue.append("; Max-Age=").append(maxAgeSeconds);
headerValue.append("; Path=/; HttpOnly; SameSite=Lax");
if (secureCookie) {
headerValue.append("; Secure");
}
response.addHeader("Set-Cookie", headerValue.toString());
}
private Object faceAiResolveUser(javax.servlet.jsp.PageContext pageContext) {
if (pageContext == null) {
return null;
}
Object[] candidates = new Object[] {
pageContext.findAttribute("user"),
pageContext.findAttribute("utenteLogon")
};
for (int index = 0; index < candidates.length; index++) {
Object candidate = candidates[index];
if (candidate != null && faceAiUserId(candidate) > 0L) {
return candidate;
}
}
return null;
}
private Object faceAiResolveUserFromSession(javax.servlet.jsp.PageContext pageContext) {
if (pageContext == null || pageContext.getSession() == null) {
return null;
}
Object loginUserId = pageContext.getSession().getAttribute("loginUser_id");
long resolvedLoginUserId = 0L;
if (loginUserId instanceof Number) {
resolvedLoginUserId = ((Number) loginUserId).longValue();
} else if (loginUserId != null) {
try {
resolvedLoginUserId = Long.parseLong(String.valueOf(loginUserId));
} catch (NumberFormatException ignored) {
resolvedLoginUserId = 0L;
}
}
if (resolvedLoginUserId <= 0L) {
return null;
}
Object sessionUser = pageContext.findAttribute("utenteLogon");
if (sessionUser == null) {
return null;
}
if (faceAiUserId(sessionUser) == resolvedLoginUserId) {
return sessionUser;
}
try {
Method findByPrimaryKey = sessionUser.getClass().getMethod("findByPrimaryKey", long.class);
findByPrimaryKey.invoke(sessionUser, Long.valueOf(resolvedLoginUserId));
} catch (NoSuchMethodException missingPrimitiveOverload) {
try {
Method findByPrimaryKey = sessionUser.getClass().getMethod("findByPrimaryKey", Long.class);
findByPrimaryKey.invoke(sessionUser, new Long(resolvedLoginUserId));
} catch (Exception ignored) {
return null;
}
} catch (Exception ignored) {
return null;
}
return faceAiUserId(sessionUser) > 0L ? sessionUser : null;
}
%>
<%
String faceAiCookieName = faceAiCookieEnv("FACEAI_IDENTITY_COOKIE", "rus_faceai_identity");
String faceAiCookieSecret = faceAiCookieEnv("FACEAI_SHARED_SECRET", "disagio-spaghetti-science-lol-boh");
Object faceAiRequestUser = pageContext.findAttribute("user");
Object faceAiSessionUser = pageContext.findAttribute("utenteLogon");
long faceAiRequestUserId = faceAiUserId(faceAiRequestUser);
long faceAiSessionUserId = faceAiUserId(faceAiSessionUser);
Object faceAiCookieUser = faceAiRequestUserId > 0L ? faceAiRequestUser : (faceAiSessionUserId > 0L ? faceAiSessionUser : faceAiResolveUserFromSession(pageContext));
boolean faceAiSecureCookie = faceAiRequestIsSecure(request);
if (faceAiFeatureEnabled && faceAiCookieUser != null && faceAiUserId(faceAiCookieUser) > 0L) {
long faceAiExpiresAt = System.currentTimeMillis() + (30L * 60L * 1000L);
try {
String faceAiToken = faceAiIdentityToken(faceAiCookieUser, faceAiCookieSecret, faceAiExpiresAt);
faceAiWriteCookieHeader(response, faceAiCookieName, faceAiToken, 30 * 60, faceAiSecureCookie);
} catch (Exception faceAiIdentityError) {
faceAiWriteCookieHeader(response, faceAiCookieName, "", 0, faceAiSecureCookie);
log("Unable to mint FaceAI identity cookie", faceAiIdentityError);
}
} else {
faceAiWriteCookieHeader(response, faceAiCookieName, "", 0, faceAiSecureCookie);
}
%>

55
www/_js/lang.js Normal file
View file

@ -0,0 +1,55 @@
// JavaScript Document
////////////////////////////////////////////
// gestione cambio lingua
////////////////////////////////////////////
var userLang = navigator.language || navigator.userLanguage;
console.log(userLang);
function changeLang(lang) {
var page = window.location.href;
var idx = page.lastIndexOf(".");
if (idx > 0) {
var ext = page.substring(idx, page.length);
if (ext.indexOf("eu") >= 0 || ext.indexOf("com") >= 0 || ext.indexOf("net") >= 0 || ext.indexOf("it") >= 0) {
ext = "";
var page1 = page;
}
else
var page1 = page.substring(0, idx);
}
else {
// caso di pagina root www.xxxx.com/
var ext = "";
var page1 = page;
}
// 1 se ho pagine del tipo xxx.html --> aggiungo -lang.html
// 2 se ho pagine del tipo xxx_xxx-xxx-xxx-.html --> sostituisco l'ultima parte con xxx-lang.html
// 3 se ho pagine del tipo xxx_xxx-xx-xx-lang.html --> sostituisco l'ultima parte
// 4 se non ho estensione sono nella root e finisce con /(www.xxxx.com/) --> vado su index-lang
// 5 altrimenti vado su index-lang.jsp
// 6 caso Ordine.abl --> ritorno alla home
if (ext == ".abl") {
theSvlt = "index-" + lang + ".html";
}
else if (ext == "") {
theSvlt = page1 + "index-" + lang + ".html";
}
else if (page1.lastIndexOf("-") == (page1.length - 1)) {
theSvlt = page1 + lang + ext;
}
else if (page1.lastIndexOf("-") == (page1.length - 3)) {
theSvlt = page1.substring(0, page1.length - 2) + lang + ext;
}
else if (page1.substring(page1.length - 1, page1.length) != "-") {
theSvlt = page1 + "-" + lang + ext;
}
else {
theSvlt = "index-" + lang + ".jsp";
}
location.href = theSvlt;
}

View file

@ -8,7 +8,6 @@
<%@ taglib uri="/WEB-INF/cc.tld" prefix="cc" %>
<html lang="<%=lang%>"><!-- InstanceBegin template="/Templates/rus.dwt" codeOutsideHTMLIsLocked="false" -->
<head>
<jsp:include page="_inc_lang.jsp" flush="true" />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@ -57,6 +56,10 @@ if (faceAiFeatureEnabledValue == null || faceAiFeatureEnabledValue.trim().length
}
String faceAiFeatureEnabledNormalized = faceAiFeatureEnabledValue != null ? faceAiFeatureEnabledValue.trim() : "";
boolean faceAiFeatureEnabled = !("0".equals(faceAiFeatureEnabledNormalized) || "false".equalsIgnoreCase(faceAiFeatureEnabledNormalized) || "no".equalsIgnoreCase(faceAiFeatureEnabledNormalized) || "off".equalsIgnoreCase(faceAiFeatureEnabledNormalized));
%>
<%@ include file="_inc_faceai_identity.jsp" %>
<jsp:include page="_inc_lang.jsp" flush="true" />
<%
java.util.Date faceAiRaceDate = CR.getGara().getDataGaraInizio();
String faceAiRacePathBase = CR.getGara().getPathBase() != null ? CR.getGara().getPathBase().trim() : "";
String faceAiRaceYear = "";

View file

@ -8,7 +8,6 @@
<%@ taglib uri="/WEB-INF/cc.tld" prefix="cc" %>
<html lang="<%=lang%>"><!-- InstanceBegin template="/Templates/rus.dwt" codeOutsideHTMLIsLocked="false" -->
<head>
<jsp:include page="_inc_lang.jsp" flush="true" />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@ -57,6 +56,10 @@ if (faceAiFeatureEnabledValue == null || faceAiFeatureEnabledValue.trim().length
}
String faceAiFeatureEnabledNormalized = faceAiFeatureEnabledValue != null ? faceAiFeatureEnabledValue.trim() : "";
boolean faceAiFeatureEnabled = !("0".equals(faceAiFeatureEnabledNormalized) || "false".equalsIgnoreCase(faceAiFeatureEnabledNormalized) || "no".equalsIgnoreCase(faceAiFeatureEnabledNormalized) || "off".equalsIgnoreCase(faceAiFeatureEnabledNormalized));
%>
<%@ include file="_inc_faceai_identity.jsp" %>
<jsp:include page="_inc_lang.jsp" flush="true" />
<%
java.util.Date faceAiRaceDate = CR.getGara().getDataGaraInizio();
String faceAiRacePathBase = CR.getGara().getPathBase() != null ? CR.getGara().getPathBase().trim() : "";
String faceAiRaceYear = "";

BIN
www_staged.tar Normal file

Binary file not shown.