Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ShiftOryx

Το ShiftOryx είναι multi-tenant SaaS για δημιουργία, διαχείριση και δημοσίευση προγραμμάτων βαρδιών. Ο πρώτος κλάδος είναι τα πρατήρια καυσίμων, αλλά η αρχιτεκτονική επιτρέπει μελλοντική χρήση από άλλες επιχειρήσεις με βάρδιες.
Το ShiftOryx είναι multi-tenant SaaS για δημιουργία, διαχείριση και δημοσίευση προγραμμάτων βαρδιών για καταστήματα και επιχειρήσεις με βάρδιες.

Προηγούμενα/compatibility ονόματα: `GasStation Shift Manager`, `GasStationProject`, `GasStation-main`.

Expand Down
2 changes: 1 addition & 1 deletion docs/SHIFTORYX_MASTER_ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion docs/project-brain.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

## Product Direction

Το ShiftOryx είναι multi-tenant SaaS για προγράμματα βαρδιών. Ξεκινά από πρατήρια καυσίμων αλλά ο domain model πρέπει να παραμένει επεκτάσιμος.
Το ShiftOryx είναι multi-tenant SaaS για προγράμματα βαρδιών καταστημάτων και επιχειρήσεων, με επεκτάσιμο domain model.

Current pilot:

Expand Down
2 changes: 1 addition & 1 deletion docs/project-status-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 καλύπτει τη δημιουργία, επεξεργασία, αποθήκευση, δημόσια προβολή και εξαγωγή εβδομαδιαίων και μηνιαίων προγραμμάτων.

Η εφαρμογή έχει δύο βασικές εμπειρίες:

Expand Down
4 changes: 2 additions & 2 deletions docs/saas-security-qa-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ Use this checklist before enabling tenant-gated access or deploying SaaS foundat
- `0` memberships shows:

```text
Δεν υπάρχει ενεργό πρατήριο συνδεδεμένο με αυτόν τον λογαριασμό.
Δεν υπάρχει ενεργό κατάστημα συνδεδεμένο με αυτόν τον λογαριασμό.
```

- `1` membership redirects to the tenant domain.
- `2+` memberships show `/select-tenant`.
- Tenant denial shows:

```text
Δεν έχετε πρόσβαση σε αυτό το πρατήριο.
Δεν έχετε πρόσβαση σε αυτό το κατάστημα.
```

## Tenant Gate
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
61 changes: 61 additions & 0 deletions scripts/validate-product-terminology.mjs
Original file line number Diff line number Diff line change
@@ -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.`);
}
8 changes: 4 additions & 4 deletions scripts/validate-saas-foundation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down Expand Up @@ -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.');
Expand Down
2 changes: 1 addition & 1 deletion scripts/validate-tenant-authorization.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ export default function App() {
page = (
<RouteNoticePage
title="ShiftOryx Central Portal"
subtitle="Η πρόσβαση σε χώρο εργασίας απαιτεί επιλογή πρατηρίου."
message="Το κεντρικό domain shiftoryx.gr είναι η κεντρική πύλη διαχείρισης. Για πρόσβαση στο πρόγραμμα του πρατηρίου σας, συνδεθείτε για να επιλέξετε πρατήριο ή επισκεφθείτε απευθείας τη διεύθυνση του πρατηρίου σας (π.χ. bp-kallis.shiftoryx.gr)."
subtitle="Η πρόσβαση σε χώρο εργασίας απαιτεί επιλογή καταστήματος."
message="Το κεντρικό domain shiftoryx.gr είναι η κεντρική πύλη διαχείρισης. Για πρόσβαση στο πρόγραμμα του καταστήματός σας, συνδεθείτε για να επιλέξετε κατάστημα ή επισκεφθείτε απευθείας τη διεύθυνση του καταστήματός σας (π.χ. bp-kallis.shiftoryx.gr)."
actionHref="/stores"
actionLabel="Τα Καταστήματά μου"
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/auth/TenantGate.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export default function TenantGate({ children, hostContext: providedHostContext,
}, [hostContext.hostname, hostContext.mode, routePath, shouldBypassGate]);

if (state.status === 'checking') {
return <TenantGateMessage title="Έλεγχος πρόσβασης" message="Ελέγχουμε την πρόσβαση στο συγκεκριμένο πρατήριο." />;
return <TenantGateMessage title="Έλεγχος πρόσβασης" message="Ελέγχουμε την πρόσβαση στο συγκεκριμένο κατάστημα." />;
}

if (state.status === 'denied') {
Expand Down
4 changes: 2 additions & 2 deletions src/components/portal/InvalidHostNotice.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export default function InvalidHostNotice({ hostContext }) {
>
<RouteNoticePage
title="Μη υποστηριζόμενη διεύθυνση"
subtitle="Το σύστημα δεν μπόρεσε να ταυτοποιήσει το πρατήριο ή την υπηρεσία."
message="Η διεύθυνση που ζητήσατε δεν αντιστοιχεί σε έγκυρο πρατήριο ή ενεργή υπηρεσία του ShiftOryx."
subtitle="Το σύστημα δεν μπόρεσε να ταυτοποιήσει το κατάστημα ή την υπηρεσία."
message="Η διεύθυνση που ζητήσατε δεν αντιστοιχεί σε έγκυρο κατάστημα ή ενεργή υπηρεσία του ShiftOryx."
actionHref={centralUrl}
actionLabel="Μετάβαση στο κεντρικό portal"
/>
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useSchedulerStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/scheduler-engine/generateSchedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
2 changes: 1 addition & 1 deletion src/services/tenantAccessService.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '../utils/tenantHostContext';

export const TENANT_ACCESS_MESSAGES = {
noAccess: 'Δεν υπάρχει ενεργό πρατήριο συνδεδεμένο με αυτόν τον λογαριασμό.',
noAccess: 'Δεν υπάρχει ενεργό κατάστημα συνδεδεμένο με αυτόν τον λογαριασμό.',
denied: TENANT_ACCESS_DENIED_MESSAGE,
};

Expand Down
2 changes: 1 addition & 1 deletion src/services/tenantAuthorization.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/utils/portalHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down