diff --git a/README.md b/README.md index 8a368d5..065725e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ShiftOryx -Το ShiftOryx είναι multi-tenant SaaS για δημιουργία, διαχείριση και δημοσίευση προγραμμάτων βαρδιών. Ο πρώτος κλάδος είναι τα πρατήρια καυσίμων, αλλά η αρχιτεκτονική επιτρέπει μελλοντική χρήση από άλλες επιχειρήσεις με βάρδιες. +Το ShiftOryx είναι multi-tenant SaaS για δημιουργία, διαχείριση και δημοσίευση προγραμμάτων βαρδιών για καταστήματα και επιχειρήσεις με βάρδιες. Προηγούμενα/compatibility ονόματα: `GasStation Shift Manager`, `GasStationProject`, `GasStation-main`. diff --git a/docs/SHIFTORYX_MASTER_ROADMAP.md b/docs/SHIFTORYX_MASTER_ROADMAP.md index 44bd8e1..6d8b164 100644 --- a/docs/SHIFTORYX_MASTER_ROADMAP.md +++ b/docs/SHIFTORYX_MASTER_ROADMAP.md @@ -40,7 +40,7 @@ Current domain status: `PURCHASED_NOT_CONFIGURED`. Ownership is confirmed, but D Όνομα: ShiftOryx. -Το ShiftOryx είναι multi-tenant SaaS για δημιουργία, διαχείριση και δημοσίευση προγραμμάτων βαρδιών. Αρχικός κλάδος είναι τα πρατήρια καυσίμων, με δυνατότητα επέκτασης σε άλλες επιχειρήσεις με βάρδιες. +Το ShiftOryx είναι multi-tenant SaaS για δημιουργία, διαχείριση και δημοσίευση προγραμμάτων βαρδιών για καταστήματα και επιχειρήσεις με βάρδιες. Κύρια URLs: diff --git a/docs/project-brain.md b/docs/project-brain.md index 3b2084c..fad562e 100644 --- a/docs/project-brain.md +++ b/docs/project-brain.md @@ -14,7 +14,7 @@ ## Product Direction -Το ShiftOryx είναι multi-tenant SaaS για προγράμματα βαρδιών. Ξεκινά από πρατήρια καυσίμων αλλά ο domain model πρέπει να παραμένει επεκτάσιμος. +Το ShiftOryx είναι multi-tenant SaaS για προγράμματα βαρδιών καταστημάτων και επιχειρήσεων, με επεκτάσιμο domain model. Current pilot: diff --git a/docs/project-status-report.md b/docs/project-status-report.md index e8b6e9b..68da34a 100644 --- a/docs/project-status-report.md +++ b/docs/project-status-report.md @@ -16,7 +16,7 @@ Master product direction: `ShiftOryx - Master Product, Technical Architecture & ## 1. Executive Summary -Το ShiftOryx είναι multi-tenant SaaS κατεύθυνση για προγράμματα βαρδιών, με πρώτο κλάδο τα πρατήρια καυσίμων. Το σημερινό BP Kallis pilot καλύπτει τη δημιουργία, επεξεργασία, αποθήκευση, δημόσια προβολή και εξαγωγή εβδομαδιαίων και μηνιαίων προγραμμάτων. +Το ShiftOryx είναι multi-tenant SaaS πλατφόρμα για προγράμματα βαρδιών καταστημάτων και επιχειρήσεων. Το σημερινό BP Kallis pilot καλύπτει τη δημιουργία, επεξεργασία, αποθήκευση, δημόσια προβολή και εξαγωγή εβδομαδιαίων και μηνιαίων προγραμμάτων. Η εφαρμογή έχει δύο βασικές εμπειρίες: diff --git a/docs/saas-security-qa-checklist.md b/docs/saas-security-qa-checklist.md index cef9989..5171e55 100644 --- a/docs/saas-security-qa-checklist.md +++ b/docs/saas-security-qa-checklist.md @@ -47,7 +47,7 @@ Use this checklist before enabling tenant-gated access or deploying SaaS foundat - `0` memberships shows: ```text -Δεν υπάρχει ενεργό πρατήριο συνδεδεμένο με αυτόν τον λογαριασμό. +Δεν υπάρχει ενεργό κατάστημα συνδεδεμένο με αυτόν τον λογαριασμό. ``` - `1` membership redirects to the tenant domain. @@ -55,7 +55,7 @@ Use this checklist before enabling tenant-gated access or deploying SaaS foundat - Tenant denial shows: ```text -Δεν έχετε πρόσβαση σε αυτό το πρατήριο. +Δεν έχετε πρόσβαση σε αυτό το κατάστημα. ``` ## Tenant Gate diff --git a/package.json b/package.json index fe8cda0..71d17a4 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "security:hardening": "node scripts/validate-security-hardening.mjs", "security:scan": "npm run security:hardening && npm run security:integrity && npm run security:audit && npm run security:cve", "qa:central-portal-isolation": "node scripts/validate-central-portal-isolation.mjs", - "qa:auth-broker-negative": "node scripts/test-auth-broker-negative-matrix.mjs" + "qa:auth-broker-negative": "node scripts/test-auth-broker-negative-matrix.mjs", + "qa:product-terminology": "node scripts/validate-product-terminology.mjs" }, "dependencies": { "@dnd-kit/core": "^6.2.0", diff --git a/scripts/validate-product-terminology.mjs b/scripts/validate-product-terminology.mjs new file mode 100644 index 0000000..1f24680 --- /dev/null +++ b/scripts/validate-product-terminology.mjs @@ -0,0 +1,61 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const SRC_DIR = path.resolve(process.cwd(), 'src'); + +// Terms that must NOT appear in runtime customer-facing code +const FORBIDDEN_PATTERNS = [ + { pattern: /πρατήρ/i, name: 'Greek fuel station term (πρατήριο / πρατήρια / πρατηρίου)' }, + { pattern: /\bfuel station\b/i, name: 'English fuel station term' }, + { pattern: /\bgas station\b/i, name: 'English gas station term (customer-facing)' }, +]; + +// Explicit allowlist for non-customer-facing technical compatibility identifiers +const ALLOWLIST = [ + 'FUEL_STATION', // Category enum identifier +]; + +function scanDirectory(dir, fileList = []) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + scanDirectory(fullPath, fileList); + } else if (/\.(jsx?|tsx?|html|css)$/.test(entry.name)) { + fileList.push(fullPath); + } + } + return fileList; +} + +const files = scanDirectory(SRC_DIR); +let violations = 0; + +for (const file of files) { + const content = fs.readFileSync(file, 'utf8'); + const lines = content.split('\n'); + + lines.forEach((line, index) => { + // Strip allowed technical enum identifiers before checking + let strippedLine = line; + for (const allowed of ALLOWLIST) { + strippedLine = strippedLine.replaceAll(allowed, ''); + } + + for (const { pattern, name } of FORBIDDEN_PATTERNS) { + if (pattern.test(strippedLine)) { + console.error( + `[TERMINOLOGY VIOLATION] ${name} found in ${path.relative(process.cwd(), file)}:${index + 1}\n > ${line.trim()}` + ); + violations++; + } + } + }); +} + +if (violations > 0) { + console.error(`\nProduct terminology validation failed with ${violations} violation(s).`); + process.exit(1); +} else { + console.log(`Product terminology validation passed. Scanned ${files.length} runtime files with zero customer-facing fuel station terms.`); +} diff --git a/scripts/validate-saas-foundation.mjs b/scripts/validate-saas-foundation.mjs index f751156..36438fd 100644 --- a/scripts/validate-saas-foundation.mjs +++ b/scripts/validate-saas-foundation.mjs @@ -86,8 +86,8 @@ assert(dockerfile.includes('nginx'), 'Dockerfile must serve the production build assert(compose.includes('gasstation-frontend'), 'docker-compose must define the frontend service.'); assert(nginx.includes('try_files $uri $uri/ /index.html'), 'Nginx must provide SPA fallback.'); assert(saasSecurityChecklist.includes('Αν υπάρχει λογαριασμός με αυτό το email'), 'SaaS security QA must cover generic forgot-password messaging.'); -assert(saasSecurityChecklist.includes('Δεν υπάρχει ενεργό πρατήριο'), 'SaaS security QA must cover no-access messaging.'); -assert(saasSecurityChecklist.includes('Δεν έχετε πρόσβαση σε αυτό το πρατήριο'), 'SaaS security QA must cover tenant denied messaging.'); +assert(saasSecurityChecklist.includes('Δεν υπάρχει ενεργό κατάστημα'), 'SaaS security QA must cover no-access messaging.'); +assert(saasSecurityChecklist.includes('Δεν έχετε πρόσβαση σε αυτό το κατάστημα'), 'SaaS security QA must cover tenant denied messaging.'); assert(saasSecurityChecklist.includes('tenantMemberships/{uid}_{tenantId}'), 'SaaS security QA must cover uid-based memberships.'); assert(saasSecurityChecklist.includes('VITE_ENABLE_TENANT_GATE=false'), 'SaaS security QA must document the pilot-safe tenant gate flag.'); assert(saasSecurityChecklist.includes('No service account JSON'), 'SaaS security QA must cover secret commit prevention.'); @@ -138,10 +138,10 @@ assert(repositories.includes('tenantTokenRequestsRepository'), 'Repository expor assert(tenantAccessService.includes('listActiveTenantAccessForUser'), 'Tenant access service must load active memberships by uid.'); assert(tenantAccessService.includes('verifyTenantAccessForHost'), 'Tenant access service must prepare tenant host membership checks.'); assert(tenantAccessService.includes('getTenantById(hostContext.tenantSlug)'), 'Tenant host access must read tenants/{tenantSlug} directly to satisfy membership-based Firestore rules.'); -assert(tenantAccessService.includes('Δεν υπάρχει ενεργό πρατήριο'), 'Tenant access service must include safe no-access message.'); +assert(tenantAccessService.includes('Δεν υπάρχει ενεργό κατάστημα'), 'Tenant access service must include safe no-access message.'); assert( tenantAccessService.includes('TENANT_ACCESS_DENIED_MESSAGE') && - tenantAuthorization.includes('Δεν έχετε πρόσβαση σε αυτό το πρατήριο'), + tenantAuthorization.includes('Δεν έχετε πρόσβαση σε αυτό το κατάστημα'), 'Tenant access service must use the shared safe denied message.', ); assert(!tenantAccessService.includes('@'), 'Tenant access service must not hardcode email access rules.'); diff --git a/scripts/validate-tenant-authorization.mjs b/scripts/validate-tenant-authorization.mjs index 56691e7..ab335e3 100644 --- a/scripts/validate-tenant-authorization.mjs +++ b/scripts/validate-tenant-authorization.mjs @@ -12,7 +12,7 @@ function assert(condition, message) { function assertDenied(result, reason, message) { assert(result.allowed === false, `${message}: expected denied.`); assert(result.reason === reason, `${message}: expected reason ${reason}, got ${result.reason}.`); - assert(result.message === 'Δεν έχετε πρόσβαση σε αυτό το πρατήριο.', `${message}: expected safe Greek denial.`); + assert(result.message === 'Δεν έχετε πρόσβαση σε αυτό το κατάστημα.', `${message}: expected safe Greek denial.`); } function assertAllowed(result, message) { diff --git a/src/App.jsx b/src/App.jsx index cd36250..1ca230f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -88,8 +88,8 @@ export default function App() { page = ( diff --git a/src/components/auth/TenantGate.jsx b/src/components/auth/TenantGate.jsx index e87ca63..bdb303f 100644 --- a/src/components/auth/TenantGate.jsx +++ b/src/components/auth/TenantGate.jsx @@ -127,7 +127,7 @@ export default function TenantGate({ children, hostContext: providedHostContext, }, [hostContext.hostname, hostContext.mode, routePath, shouldBypassGate]); if (state.status === 'checking') { - return ; + return ; } if (state.status === 'denied') { diff --git a/src/components/portal/InvalidHostNotice.jsx b/src/components/portal/InvalidHostNotice.jsx index a77d34a..84e7480 100644 --- a/src/components/portal/InvalidHostNotice.jsx +++ b/src/components/portal/InvalidHostNotice.jsx @@ -19,8 +19,8 @@ export default function InvalidHostNotice({ hostContext }) { > diff --git a/src/hooks/useSchedulerStore.js b/src/hooks/useSchedulerStore.js index 947c5cf..a994072 100644 --- a/src/hooks/useSchedulerStore.js +++ b/src/hooks/useSchedulerStore.js @@ -136,7 +136,7 @@ function getCurrentYearMonth() { function getPublicTenantId() { const tenantSlug = getCurrentTenantHostContext()?.tenantSlug; if (!tenantSlug) { - throw new Error('Δεν βρέθηκε tenant context για το πρατήριο.'); + throw new Error('Δεν βρέθηκε tenant context για το κατάστημα.'); } return tenantSlug; } diff --git a/src/scheduler-engine/generateSchedule.ts b/src/scheduler-engine/generateSchedule.ts index c2db584..8f0d19e 100644 --- a/src/scheduler-engine/generateSchedule.ts +++ b/src/scheduler-engine/generateSchedule.ts @@ -46,7 +46,7 @@ export function generateSchedule(input: GenerateScheduleInput): GenerateSchedule }; } - // 2. Check if input matches legacy 4-6 Fuel Station topology + // 2. Check if input matches legacy 4-6 Store topology const hasBaseRoles = enabledEmployees.length >= 4 && enabledEmployees.length <= 6 && diff --git a/src/services/tenantAccessService.js b/src/services/tenantAccessService.js index 2127065..049dab4 100644 --- a/src/services/tenantAccessService.js +++ b/src/services/tenantAccessService.js @@ -10,7 +10,7 @@ import { } from '../utils/tenantHostContext'; export const TENANT_ACCESS_MESSAGES = { - noAccess: 'Δεν υπάρχει ενεργό πρατήριο συνδεδεμένο με αυτόν τον λογαριασμό.', + noAccess: 'Δεν υπάρχει ενεργό κατάστημα συνδεδεμένο με αυτόν τον λογαριασμό.', denied: TENANT_ACCESS_DENIED_MESSAGE, }; diff --git a/src/services/tenantAuthorization.js b/src/services/tenantAuthorization.js index 24684a3..dad06ac 100644 --- a/src/services/tenantAuthorization.js +++ b/src/services/tenantAuthorization.js @@ -1,7 +1,7 @@ export const ACTIVE_MEMBERSHIP_STATUS = 'ACTIVE'; export const INACTIVE_MEMBERSHIP_STATUSES = ['INACTIVE', 'SUSPENDED', 'EXPIRED', 'REVOKED']; export const ADMIN_TENANT_ROLES = ['OWNER']; -export const TENANT_ACCESS_DENIED_MESSAGE = 'Δεν έχετε πρόσβαση σε αυτό το πρατήριο.'; +export const TENANT_ACCESS_DENIED_MESSAGE = 'Δεν έχετε πρόσβαση σε αυτό το κατάστημα.'; function normalizeToken(value) { return String(value || '').trim().toUpperCase(); diff --git a/src/utils/portalHelpers.js b/src/utils/portalHelpers.js index cd7eafb..080d9dc 100644 --- a/src/utils/portalHelpers.js +++ b/src/utils/portalHelpers.js @@ -35,7 +35,7 @@ export const VALID_BUSINESS_CATEGORIES = Object.freeze([ export const BUSINESS_CATEGORY_OPTIONS = Object.freeze([ { id: 'OTHER', label: 'Άλλη Επιχείρηση' }, - { id: 'FUEL_STATION', label: 'Πρατήριο Καυσίμων' }, + { id: 'FUEL_STATION', label: 'Κατάστημα' }, { id: 'CAFE', label: 'Καφέ / Bistro' }, { id: 'RESTAURANT', label: 'Εστιατόριο / Εστίαση' }, { id: 'HAIR_SALON', label: 'Κομμωτήριο / Barber' },