Livrare Lot 3 (Frontend): aplicație web, aplicație mobilă Android, extensie browser

- Surse complete web (React/Vite) + mobil (React Native/Expo) + extensie (MV3)
- Documentație de livrare: ghid utilizare, matrice trasabilitate cerințe, raport testare furnizor
- Artefacte binare: imagine Docker didi-frontend:lot3-1.0, APK, extensie v3.2.6 + SHA256SUMS
- Configurare adresă platformă externalizată (build args / .env / config.js)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Top Clossers 2026-07-17 12:33:59 +03:00
commit cec967f953
321 changed files with 80506 additions and 0 deletions

189
extension/auth.js Normal file
View file

@ -0,0 +1,189 @@
// DIDI Analysis Tool — OIDC/OAuth2 PKCE auth module (Keycloak, realm didi-clients)
// Loaded in the MV3 service worker via importScripts. Exposes self.DidiAuth.
//
// Flow: chrome.identity.launchWebAuthFlow → Keycloak authorization endpoint
// (PKCE S256 + state) → code exchange on the token endpoint → tokens persisted
// in chrome.storage.local. Access token auto-refreshes; logout revokes the
// Keycloak session (end_session with refresh_token).
(() => {
// Adresele mediului vin exclusiv din config.js (self.DidiConfig) — vezi acolo și
// explicația schemei HTTPS (pagina de login) / HTTP (token & API) pentru LAN.
const { KC_NAV_BASE, KC_API_BASE, REALM, CLIENT_ID } = self.DidiConfig;
const REDIRECT_URI = `https://${chrome.runtime.id}.chromiumapp.org/oidc`;
const OIDC = {
auth: `${KC_NAV_BASE}/realms/${REALM}/protocol/openid-connect/auth`,
token: `${KC_API_BASE}/realms/${REALM}/protocol/openid-connect/token`,
logout: `${KC_API_BASE}/realms/${REALM}/protocol/openid-connect/logout`,
};
const STORAGE_KEY = 'didiTokens';
const REFRESH_SKEW_MS = 30 * 1000; // refresh 30s before expiry
// ===== helpers =====
function base64UrlEncode(bytes) {
let str = '';
for (const b of bytes) str += String.fromCharCode(b);
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function randomString(byteLen = 32) {
const bytes = new Uint8Array(byteLen);
crypto.getRandomValues(bytes);
return base64UrlEncode(bytes);
}
async function sha256Base64Url(input) {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
return base64UrlEncode(new Uint8Array(digest));
}
function decodeJwtPayload(token) {
try {
const payload = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(atob(payload));
} catch {
return {};
}
}
async function getStoredTokens() {
const data = await chrome.storage.local.get([STORAGE_KEY]);
return data[STORAGE_KEY] || null;
}
async function storeTokens(tokenResponse) {
const claims = decodeJwtPayload(tokenResponse.access_token);
const tokens = {
access_token: tokenResponse.access_token,
refresh_token: tokenResponse.refresh_token,
expires_at: Date.now() + (tokenResponse.expires_in || 60) * 1000,
sub: claims.sub || null,
email: claims.email || null,
name: claims.name || claims.preferred_username || null,
};
await chrome.storage.local.set({ [STORAGE_KEY]: tokens });
return tokens;
}
// ===== login (interactive) =====
async function login() {
const codeVerifier = randomString(48);
const codeChallenge = await sha256Base64Url(codeVerifier);
const state = randomString(16);
const nonce = randomString(16);
const authUrl = `${OIDC.auth}?` + new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'openid profile email',
state,
nonce,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
}).toString();
const redirectUrl = await chrome.identity.launchWebAuthFlow({ url: authUrl, interactive: true });
if (!redirectUrl) throw new Error('Autentificare anulată');
const params = new URL(redirectUrl).searchParams;
if (params.get('error')) {
throw new Error(params.get('error_description') || params.get('error'));
}
if (params.get('state') !== state) {
throw new Error('State mismatch (posibil CSRF) — autentificare respinsă');
}
const code = params.get('code');
if (!code) throw new Error('Răspuns fără cod de autorizare');
const resp = await fetch(OIDC.token, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
code,
redirect_uri: REDIRECT_URI,
code_verifier: codeVerifier,
}),
});
if (!resp.ok) {
throw new Error(`Schimbul de cod a eșuat: ${resp.status} ${await resp.text()}`);
}
return storeTokens(await resp.json());
}
// ===== token refresh (single-flight) =====
let refreshPromise = null;
async function refreshTokens(refreshToken) {
if (!refreshPromise) {
refreshPromise = (async () => {
const resp = await fetch(OIDC.token, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: CLIENT_ID,
refresh_token: refreshToken,
}),
});
if (!resp.ok) {
await chrome.storage.local.remove([STORAGE_KEY]);
throw new Error('Sesiune expirată — autentifică-te din nou');
}
return storeTokens(await resp.json());
})().finally(() => { refreshPromise = null; });
}
return refreshPromise;
}
/** Returns a valid access token, refreshing it if needed. Throws if logged out. */
async function getValidToken() {
let tokens = await getStoredTokens();
if (!tokens?.access_token) {
throw new Error('Neautentificat. Deschide extensia și apasă Login.');
}
if (Date.now() >= tokens.expires_at - REFRESH_SKEW_MS) {
tokens = await refreshTokens(tokens.refresh_token);
}
return tokens.access_token;
}
// ===== state / logout =====
async function getAuthState() {
const tokens = await getStoredTokens();
if (!tokens?.access_token) return { authenticated: false };
return { authenticated: true, email: tokens.email, name: tokens.name, sub: tokens.sub };
}
/** User identity claims for API payloads (user_id/user_email). */
async function getUserClaims() {
const tokens = await getStoredTokens();
return { user_id: tokens?.sub || '', user_email: tokens?.email || '' };
}
async function logout() {
const tokens = await getStoredTokens();
if (tokens?.refresh_token) {
try {
await fetch(OIDC.logout, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ client_id: CLIENT_ID, refresh_token: tokens.refresh_token }),
});
} catch (e) {
console.warn('[AUTH] Keycloak logout failed (clearing local session anyway):', e.message);
}
}
await chrome.storage.local.remove([STORAGE_KEY]);
}
self.DidiAuth = { login, logout, getValidToken, getAuthState, getUserClaims };
})();