feat: Add FaceAI integration with handoff and return functionality
- Introduced a new workspace for FaceAI in package.json. - Implemented FaceAI handoff logic in faceai_handoff.php, including identity verification and token signing. - Created faceai_return.php to handle return requests from FaceAI, validating tokens and forwarding results. - Developed faceai_simulator.php and faceai_simulator_view.php for simulating the FaceAI interface with demo photos. - Enhanced rus-ecom-240621.js to support new FaceAI features, including dynamic URL building and button integration. - Added faceai_config.php for configuration management, including environment variable handling and utility functions. - Updated HTML structure and styles in simulator view for better user experience.
This commit is contained in:
parent
f65a85dcc9
commit
da362c201f
31 changed files with 4511 additions and 60 deletions
3
faceai/apps/frontend/src/App.vue
Normal file
3
faceai/apps/frontend/src/App.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
57
faceai/apps/frontend/src/components/LegacyHeader.vue
Normal file
57
faceai/apps/frontend/src/components/LegacyHeader.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<script setup>
|
||||
import { legacyAsset } from '../legacyAssets.js';
|
||||
|
||||
const logoUrl = legacyAsset('/images/layout/regalami-un-sorriso-ets-640.png');
|
||||
const facebookUrl = legacyAsset('/images/FB-f-Logo__blue_29.png');
|
||||
const donateUrl = legacyAsset('/images/btn_donateCC_LG.gif');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a id="top"></a>
|
||||
<nav class="navbar fixed-top navbar-expand-lg navbar-light bg-white fixed-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="http://localhost:8080/faceai_simulator.php?raceId=101&lang=it">
|
||||
<img :src="logoUrl" alt="Regalami Un Sorriso ETS" width="100" />
|
||||
</a>
|
||||
<button class="navbar-toggler navbar-toggler-right" type="button">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse show" id="navbarResponsive">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="http://localhost:8080/index.jsp">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="http://localhost:8080/associazione.jsp">Associazione</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="http://localhost:8080/faceai_simulator.php?raceId=101&lang=it">Foto</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link btn btn-sm btn-warning" href="http://localhost:8080/gallery2.php">Archivio</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="http://localhost:8080/dettaglio_clienti-it.html">
|
||||
<img :src="donateUrl" border="0" alt="PayPal" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#">
|
||||
<i class="fa fa-user" aria-hidden="true"></i> Il mio account
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://it-it.facebook.com/pg/Regalami-un-sorriso-ETS-189377806523/community/">
|
||||
<img :src="facebookUrl" class="img-fluid" alt="Facebook" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
26
faceai/apps/frontend/src/legacyAssets.js
Normal file
26
faceai/apps/frontend/src/legacyAssets.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const legacyAssetBaseUrl = (import.meta.env.VITE_LEGACY_ASSET_BASE_URL || '/legacy-static').replace(/\/$/, '');
|
||||
|
||||
export function legacyAsset(path) {
|
||||
return `${legacyAssetBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
export function injectLegacyStylesheets() {
|
||||
const stylesheets = [
|
||||
legacyAsset('/vendor/bootstrap/css/bootstrap.min.css'),
|
||||
legacyAsset('/css/font-awesome.min.css'),
|
||||
'https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i',
|
||||
legacyAsset('/css/custom-style.css')
|
||||
];
|
||||
|
||||
stylesheets.forEach((href) => {
|
||||
if (document.head.querySelector(`link[data-legacy-href="${href}"]`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = href;
|
||||
link.dataset.legacyHref = href;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
8
faceai/apps/frontend/src/main.js
Normal file
8
faceai/apps/frontend/src/main.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router.js';
|
||||
import './styles.css';
|
||||
import { injectLegacyStylesheets } from './legacyAssets.js';
|
||||
|
||||
injectLegacyStylesheets();
|
||||
createApp(App).use(router).mount('#app');
|
||||
17
faceai/apps/frontend/src/router.js
Normal file
17
faceai/apps/frontend/src/router.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import HomeView from './views/HomeView.vue';
|
||||
import HandoffCallbackView from './views/HandoffCallbackView.vue';
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: HomeView
|
||||
},
|
||||
{
|
||||
path: '/auth/callback',
|
||||
component: HandoffCallbackView
|
||||
}
|
||||
]
|
||||
});
|
||||
65
faceai/apps/frontend/src/styles.css
Normal file
65
faceai/apps/frontend/src/styles.css
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
body {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.faceai-page {
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.faceai-form-shell {
|
||||
padding: 0 0 12px;
|
||||
}
|
||||
|
||||
.faceai-action-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.faceai-feedback {
|
||||
background: #f8f9fa;
|
||||
border-left: 5px solid #fe3d00;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.faceai-feedback .lead {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.faceai-spinner-block {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-weight: 500;
|
||||
color: #5b4938;
|
||||
}
|
||||
|
||||
.faceai-spinner-block .spinner-border {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.callback-shell {
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.callback-card {
|
||||
width: 100%;
|
||||
max-width: 620px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
padding: 24px;
|
||||
margin: 40px auto;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.faceai-action-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.faceai-action-row .btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
51
faceai/apps/frontend/src/views/HandoffCallbackView.vue
Normal file
51
faceai/apps/frontend/src/views/HandoffCallbackView.vue
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<script setup>
|
||||
import LegacyHeader from '../components/LegacyHeader.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const errorMessage = ref('');
|
||||
|
||||
onMounted(async () => {
|
||||
const token = route.query.token;
|
||||
|
||||
if (!token) {
|
||||
errorMessage.value = 'Missing handoff token.';
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/auth/exchange', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ token })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => ({ error: 'Token exchange failed' }));
|
||||
errorMessage.value = payload.error || 'Token exchange failed';
|
||||
return;
|
||||
}
|
||||
|
||||
router.replace('/');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<LegacyHeader />
|
||||
<div class="container my-3 callback-shell">
|
||||
<div class="callback-card">
|
||||
<h1 class="my-4">Face ID</h1>
|
||||
<div v-if="!errorMessage" class="faceai-spinner-block">
|
||||
<span class="spinner-border text-warning" role="status" aria-hidden="true"></span>
|
||||
<span>Validazione handoff legacy e creazione della sessione FaceAI in corso...</span>
|
||||
</div>
|
||||
<p v-else class="text-danger">{{ errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
235
faceai/apps/frontend/src/views/HomeView.vue
Normal file
235
faceai/apps/frontend/src/views/HomeView.vue
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import LegacyHeader from '../components/LegacyHeader.vue';
|
||||
import { legacyAsset } from '../legacyAssets.js';
|
||||
|
||||
const coverImageUrl = legacyAsset('/images/layout/Logo_RUS_ETS_tricolore_3-1.jpg');
|
||||
|
||||
const session = ref(null);
|
||||
const loading = ref(true);
|
||||
const errorMessage = ref('');
|
||||
const selectedFile = ref(null);
|
||||
const activeSearch = ref(null);
|
||||
const redirectUrl = ref('');
|
||||
const isSubmitting = ref(false);
|
||||
const isRedirecting = ref(false);
|
||||
let pollTimer = null;
|
||||
|
||||
const isWorking = computed(() => loading.value || isSubmitting.value || isRedirecting.value || activeSearch.value?.status === 'processing');
|
||||
|
||||
const busyLabel = computed(() => {
|
||||
if (loading.value) {
|
||||
return 'Caricamento sessione FaceAI...';
|
||||
}
|
||||
|
||||
if (isSubmitting.value) {
|
||||
return 'Invio del selfie e preparazione della ricerca...';
|
||||
}
|
||||
|
||||
if (isRedirecting.value) {
|
||||
return 'Reindirizzamento alla pagina legacy filtrata in corso...';
|
||||
}
|
||||
|
||||
if (activeSearch.value?.status === 'processing') {
|
||||
return 'Ricerca biometrica in corso su tutte le foto della gara...';
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (!activeSearch.value) {
|
||||
return 'Carica un selfie per avviare una ricerca limitata alla gara corrente.';
|
||||
}
|
||||
|
||||
if (activeSearch.value.status === 'completed') {
|
||||
return `Ricerca completata. Trovate ${activeSearch.value.matchCount} foto corrispondenti.`;
|
||||
}
|
||||
|
||||
return 'Ricerca in corso. Il sistema aggiorna automaticamente lo stato finche il risultato non e pronto.';
|
||||
});
|
||||
|
||||
async function loadSession() {
|
||||
loading.value = true;
|
||||
const response = await fetch('/api/session', { credentials: 'include' });
|
||||
|
||||
if (!response.ok) {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
session.value = await response.json();
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function onFileChange(event) {
|
||||
selectedFile.value = event.target.files?.[0] || null;
|
||||
}
|
||||
|
||||
async function pollSearch(searchId) {
|
||||
const response = await fetch(`/api/searches/${searchId}`, { credentials: 'include' });
|
||||
if (!response.ok) {
|
||||
errorMessage.value = 'Unable to read search status.';
|
||||
isSubmitting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
activeSearch.value = await response.json();
|
||||
if (activeSearch.value.status === 'completed') {
|
||||
isSubmitting.value = false;
|
||||
const redirectResponse = await fetch(`/api/searches/${searchId}/redirect`, { credentials: 'include' });
|
||||
const payload = await redirectResponse.json();
|
||||
if (!redirectResponse.ok) {
|
||||
errorMessage.value = payload.error || 'Unable to build return URL.';
|
||||
return;
|
||||
}
|
||||
redirectUrl.value = payload.url;
|
||||
isRedirecting.value = true;
|
||||
window.setTimeout(() => {
|
||||
window.location.href = payload.url;
|
||||
}, 1200);
|
||||
return;
|
||||
}
|
||||
|
||||
pollTimer = window.setTimeout(() => {
|
||||
pollSearch(searchId);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
async function submitSearch() {
|
||||
errorMessage.value = '';
|
||||
redirectUrl.value = '';
|
||||
isRedirecting.value = false;
|
||||
|
||||
if (!selectedFile.value) {
|
||||
errorMessage.value = 'Choose a selfie before starting the search.';
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
const response = await fetch('/api/searches', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
raceId: session.value.race.id,
|
||||
selfieName: selectedFile.value.name
|
||||
})
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
errorMessage.value = payload.error || 'Unable to create the search.';
|
||||
isSubmitting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
activeSearch.value = payload;
|
||||
pollSearch(payload.id);
|
||||
}
|
||||
|
||||
onMounted(loadSession);
|
||||
onBeforeUnmount(() => {
|
||||
if (pollTimer) {
|
||||
window.clearTimeout(pollTimer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<LegacyHeader />
|
||||
|
||||
<div class="container my-3 faceai-page">
|
||||
<div class="row mb-5">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="my-4">Face ID</h1>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<img :src="coverImageUrl" class="img-fluid border border-warning" alt="FaceAI" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-10">
|
||||
<div class="row riepilogo">
|
||||
<div class="col-md-3">
|
||||
<p><i class="fa fa-user fa-lg text-warning" aria-hidden="true"></i> {{ session ? session.user.displayName : 'Sessione FaceAI' }}</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p><i class="fa fa-camera-retro fa-lg text-warning" aria-hidden="true"></i> {{ session ? session.race.name : 'Upload selfie' }}</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p><i class="fa fa-refresh fa-lg text-warning" aria-hidden="true"></i> {{ activeSearch ? activeSearch.status : 'ready' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="bg-light faceai-form-shell">
|
||||
<div class="row">
|
||||
<div class="form-group mx-3 pt-4 pb-1 mb-0 px-2 arrow_box">
|
||||
<h2>Cerca le tue foto</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="col-12 p-4">
|
||||
<div class="faceai-spinner-block">
|
||||
<span class="spinner-border text-warning" role="status" aria-hidden="true"></span>
|
||||
<span>{{ busyLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!session" class="col-12 p-4">
|
||||
<p>Apri prima il simulatore legacy per generare il token firmato di handoff.</p>
|
||||
<a class="btn btn-warning" href="http://localhost:8080/faceai_simulator.php?raceId=101&lang=it">Apri il simulatore legacy</a>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="form-group col-12 col-md-4 mt-3 ml-3">
|
||||
<div class="input-group">
|
||||
<label for="raceName" class="sr-only">Gara</label>
|
||||
<input id="raceName" class="form-control form-control-sm mb-2 mb-sm-0" :value="session.race.name" readonly />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-12 col-md-3 mt-3 ml-3">
|
||||
<div class="input-group">
|
||||
<label for="langView" class="sr-only">Lingua</label>
|
||||
<input id="langView" class="form-control form-control-sm mb-2 mb-sm-0" :value="session.lang" readonly />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-12 col-md-4 mt-3 ml-3">
|
||||
<div class="input-group">
|
||||
<label for="selfieUpload" class="sr-only">Selfie</label>
|
||||
<input id="selfieUpload" class="form-control form-control-sm mb-2 mb-sm-0" type="file" accept="image/*" @change="onFileChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-12 mt-2 ml-3 mr-3 faceai-action-row">
|
||||
<button class="btn btn-warning" type="button" :disabled="isWorking" @click="submitSearch">Avvia ricerca Face ID</button>
|
||||
<a class="btn btn-light" :href="session.returnUrl">Torna alla pagina gara</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faceai-feedback mt-4">
|
||||
<p class="lead mb-2">{{ statusLabel }}</p>
|
||||
<div v-if="isWorking && busyLabel" class="faceai-spinner-block mb-3">
|
||||
<span class="spinner-border spinner-border-sm text-warning" role="status" aria-hidden="true"></span>
|
||||
<span>{{ busyLabel }}</span>
|
||||
</div>
|
||||
<p v-if="activeSearch" class="mb-2">Match trovati: {{ activeSearch.matchCount }}</p>
|
||||
<p v-if="redirectUrl" class="mb-2">Reindirizzamento alla pagina legacy filtrata in corso...</p>
|
||||
<p v-if="errorMessage" class="text-danger mb-2">{{ errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue