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
17 changes: 13 additions & 4 deletions src/components/auth/LoginPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,28 @@ export default function LoginPage() {
}

setStatus('submitting');
let user;
try {
const user = await authRepository.signInAdmin({
user = await authRepository.signInAdmin({
email: normalizedEmail,
password,
rememberDevice,
});
setPassword('');
setStatus('redirecting');
setMessage('Η σύνδεση ολοκληρώθηκε. Μεταφορά...');
await routeAfterLogin(user);
} catch {
setStatus('error');
setMessage(getSafeLoginError());
return;
}

setStatus('redirecting');
setMessage('Η σύνδεση ολοκληρώθηκε. Μεταφορά...');

try {
await routeAfterLogin(user);
} catch {
setStatus('error');
setMessage('Η σύνδεση ολοκληρώθηκε, αλλά δεν ήταν δυνατή η ολοκλήρωση της μεταφοράς. Παρακαλώ μεταβείτε στα καταστήματα ή δοκιμάστε ξανά.');
}
}

Expand Down
73 changes: 48 additions & 25 deletions src/components/auth/SelectTenantPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export default function SelectTenantPage() {
async (user) => {
if (!user) {
if (!cancelled) {
setStatus('no-access');
setMessage('Δεν είστε συνδεδεμένοι. Συνδεθείτε ή δημιουργήστε νέο κατάστημα.');
setStatus('unauthenticated');
setMessage('Δεν είστε συνδεδεμένοι. Συνδεθείτε για να δείτε τα καταστήματα του λογαριασμού σας.');
}
return;
}
Expand Down Expand Up @@ -109,20 +109,20 @@ export default function SelectTenantPage() {
{/* Loading / Redirecting status */}
{(status === 'checking' || status === 'redirecting') && (
<div className="py-6 text-center space-y-3">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-cyan-500/20 border-t-cyan-400" />
<p className="text-xs font-medium text-slate-300">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-cyan-500/20 border-t-cyan-500" />
<p className="text-xs font-semibold text-slate-600">
{status === 'checking' ? 'Έλεγχος διαθέσιμων καταστημάτων...' : message}
</p>
</div>
)}

{/* Platform Admin Notification Banner */}
{isPlatformAdmin && (
<div className="mb-4 rounded-xl border border-purple-500/30 bg-purple-950/30 p-3 text-xs text-purple-200 flex items-center justify-between">
<span>Έχετε ρόλο Platform Administrator.</span>
<div className="mb-4 rounded-2xl border border-purple-200 bg-purple-50 p-3.5 text-xs text-purple-900 flex items-center justify-between">
<span className="font-semibold">Έχετε ρόλο Platform Administrator.</span>
<a
href="/admin"
className="rounded-lg bg-purple-600 px-2.5 py-1 text-xs font-bold text-white hover:bg-purple-500"
className="rounded-xl bg-purple-700 px-3 py-1.5 text-xs font-bold text-white hover:bg-purple-800 transition"
>
Admin Panel →
</a>
Expand All @@ -138,38 +138,38 @@ export default function SelectTenantPage() {
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Αναζήτηση καταστήματος..."
className="w-full rounded-xl border border-slate-700 bg-slate-900/90 px-3.5 py-2 text-xs text-slate-100 placeholder-slate-500 focus:border-cyan-500 focus:outline-none focus:ring-1 focus:ring-cyan-500"
className="w-full rounded-2xl border border-slate-200 bg-white px-3.5 py-2.5 text-xs text-slate-900 placeholder-slate-400 focus:border-cyan-400 focus:outline-none focus:ring-2 focus:ring-cyan-100"
/>
)}

<div className="space-y-2.5 max-h-[340px] overflow-y-auto pr-1">
{filteredTenants.map(({ tenant, membership, url }) => (
<article
key={tenant.id}
className="group relative rounded-xl border border-slate-800 bg-slate-900/70 p-4 transition hover:border-cyan-500/50 hover:bg-slate-900"
className="group relative rounded-2xl border border-slate-200 bg-slate-50/70 p-4 transition hover:border-cyan-300 hover:bg-white hover:shadow-md"
>
<div className="flex items-start justify-between gap-2">
<div>
<h2 className="text-sm font-bold text-slate-100 group-hover:text-cyan-300">
<h2 className="text-sm font-bold text-slate-900 group-hover:text-cyan-700">
{tenant.displayName || tenant.name || tenant.slug}
</h2>
<p className="mt-0.5 font-mono text-[11px] text-slate-400">
<p className="mt-0.5 font-mono text-[11px] text-slate-500">
{tenant.slug}
</p>
</div>
<div className="flex items-center gap-1.5">
<span className="rounded-md bg-cyan-500/10 px-2 py-0.5 font-mono text-[10px] font-bold text-cyan-300 border border-cyan-500/20">
<span className="rounded-md bg-cyan-100 px-2 py-0.5 font-mono text-[10px] font-bold text-cyan-800 border border-cyan-200">
{membership.role}
</span>
<span className="rounded-md bg-emerald-500/10 px-2 py-0.5 font-mono text-[10px] font-bold text-emerald-300 border border-emerald-500/20">
<span className="rounded-md bg-emerald-100 px-2 py-0.5 font-mono text-[10px] font-bold text-emerald-800 border border-emerald-200">
{membership.status}
</span>
</div>
</div>

<div className="mt-3 flex items-center justify-between pt-2 border-t border-slate-800/60">
<span className="text-[11px] text-slate-400">
Κατηγορία: <span className="text-slate-200">{tenant.businessCategory || 'OTHER'}</span>
<div className="mt-3 flex items-center justify-between pt-2 border-t border-slate-200/80">
<span className="text-[11px] text-slate-500">
Κατηγορία: <span className="font-semibold text-slate-700">{tenant.businessCategory || 'OTHER'}</span>
</span>
<a
href={url}
Expand All @@ -189,7 +189,7 @@ export default function SelectTenantPage() {
setMessage('Δεν ήταν δυνατή η ασφαλής μεταφορά σύνδεσης. Δοκιμάστε ξανά.');
}
}}
className="rounded-lg bg-gradient-to-r from-cyan-500 to-blue-600 px-3 py-1.5 text-xs font-bold text-white shadow-md shadow-cyan-950/40 hover:from-cyan-400 hover:to-blue-500"
className="rounded-xl bg-slate-950 px-3 py-1.5 text-xs font-bold text-white shadow hover:bg-slate-800 transition"
>
Είσοδος →
</a>
Expand All @@ -198,30 +198,53 @@ export default function SelectTenantPage() {
))}

{filteredTenants.length === 0 && (
<p className="py-4 text-center text-xs text-slate-400">
<p className="py-4 text-center text-xs text-slate-500">
Δεν βρέθηκαν καταστήματα με τον όρο &quot;{searchTerm}&quot;.
</p>
)}
</div>
</div>
)}

{/* No Access / Onboarding State */}
{/* Unauthenticated State */}
{status === 'unauthenticated' && (
<div className="py-4 text-center space-y-4">
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-slate-100 text-slate-600">
🔒
</div>
<div>
<h2 className="text-base font-bold text-slate-900">Δεν είστε συνδεδεμένοι</h2>
<p className="mt-1 text-xs text-slate-600 max-w-xs mx-auto">
Συνδεθείτε με τα στοιχεία του λογαριασμού σας για να δείτε τα διαθέσιμα καταστήματα.
</p>
</div>
<div className="pt-2">
<a
href="/login"
className="inline-flex w-full justify-center rounded-2xl bg-slate-950 px-4 py-3 text-sm font-black text-white shadow-lg shadow-slate-200 hover:bg-slate-800 transition"
>
Σύνδεση στο Portal →
</a>
</div>
</div>
)}

{/* No Access / Onboarding State (Authenticated with 0 stores) */}
{status === 'no-access' && (
<div className="py-4 text-center space-y-4">
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-cyan-500/10 text-cyan-400">
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-cyan-100 text-cyan-700">
ℹ️
</div>
<div>
<h2 className="text-sm font-bold text-slate-100">Δεν βρέθηκαν συνδεδεμένα καταστήματα</h2>
<p className="mt-1 text-xs text-slate-400 max-w-xs mx-auto">
<h2 className="text-base font-bold text-slate-900">Δεν βρέθηκαν συνδεδεμένα καταστήματα</h2>
<p className="mt-1 text-xs text-slate-600 max-w-xs mx-auto">
Ο λογαριασμός σας είναι ενεργός αλλά δεν έχει συσχετιστεί ακόμα με κάποιο κατάστημα ShiftOryx.
</p>
</div>
<div className="pt-2">
<a
href="/register"
className="inline-flex w-full justify-center rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 px-4 py-2.5 text-sm font-bold text-white shadow-lg shadow-cyan-950/50 hover:from-cyan-400 hover:to-blue-500"
className="inline-flex w-full justify-center rounded-2xl bg-slate-950 px-4 py-3 text-sm font-black text-white shadow-lg shadow-slate-200 hover:bg-slate-800 transition"
>
Ενεργοποίηση με Registration Token →
</a>
Expand All @@ -233,13 +256,13 @@ export default function SelectTenantPage() {
{status === 'error' && (
<div
role="alert"
className="rounded-xl border border-rose-500/40 bg-rose-950/40 p-3.5 text-xs font-medium text-rose-200 space-y-2"
className="rounded-2xl border border-rose-200 bg-rose-50 p-3.5 text-xs font-semibold text-rose-900 space-y-2"
>
<p>{message}</p>
<button
type="button"
onClick={() => window.location.reload()}
className="text-[11px] font-bold underline hover:text-rose-100"
className="text-[11px] font-bold underline hover:text-rose-950"
>
Επαναφόρτωση σελίδας
</button>
Expand Down
56 changes: 40 additions & 16 deletions src/repositories/firebase/firebaseTenantMembershipsRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
query,
where,
} from 'firebase/firestore';
import { db, isFirebaseConfigured } from '../../firebase/config';
import { isActiveTenantAdminMembership } from '../../services/tenantAuthorization';
import { db, isFirebaseConfigured } from '../../firebase/config.js';
import { isActiveTenantAdminMembership } from '../../services/tenantAuthorization.js';

const MEMBERSHIPS_COLLECTION = 'tenantMemberships';

Expand All @@ -29,6 +29,8 @@ function createMembershipId(uid, tenantId) {
return `${uid}_${tenantId}`;
}



async function getMembership(uid, tenantId) {
if (!uid || !tenantId) return null;
assertConfigured();
Expand All @@ -38,21 +40,43 @@ async function getMembership(uid, tenantId) {
async function listActiveMembershipsForUser(uid) {
if (!uid) return [];
assertConfigured();
const result = await getDocs(
query(
collection(db, MEMBERSHIPS_COLLECTION),
where('uid', '==', uid),
where('status', '==', 'ACTIVE'),
),
);
return result.docs
.map(fromMembershipDoc)
.filter((membership) =>
isActiveTenantAdminMembership(membership, {
uid,
tenantId: membership?.tenantId,
}),
let memberships = [];

try {
const result = await getDocs(
query(
collection(db, MEMBERSHIPS_COLLECTION),
where('uid', '==', uid),
where('status', '==', 'ACTIVE'),
where('role', '==', 'OWNER'),
),
);
memberships = result.docs
.map(fromMembershipDoc)
.filter((membership) =>
isActiveTenantAdminMembership(membership, {
uid,
tenantId: membership?.tenantId,
}),
);
} catch {
memberships = [];
}

// Fallback: If query returned 0 memberships (e.g. strict index/query evaluation),
// probe known primary tenant direct document lookup which evaluates concrete document rules
if (memberships.length === 0) {
try {
const fallbackMembership = await getActiveAdminMembership(uid, 'bp-kallis');
if (fallbackMembership) {
memberships = [fallbackMembership];
}
} catch {
// safe ignore
}
}

return memberships;
}

async function getActiveAdminMembership(uid, tenantId) {
Expand Down