Restrict app access to admin account only#4
Conversation
- Export ADMIN_EMAIL constant from auth.service as single source of truth - Block non-admin sign-ins immediately in loginWithGoogle (sign-out + ACCESS_DENIED) - Sign out any non-admin user found in auth-state effect in app.ts - authGuard, publicGuard, onboardingGuard all gate on ADMIN_EMAIL - auth.component: loading spinner + "Access restricted" error banner - Replace all 9 inline 'schoedelb@gmail.com' strings with ADMIN_EMAIL import - Lint, build, and tests all pass Agent-Logs-Url: https://github.com/schoedel-learn/LXPython/sessions/3bf1df33-dc13-468c-b98a-1bceaa6cf11e Co-authored-by: schoedel-learn <212212630+schoedel-learn@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR centralizes the admin account identifier and attempts to enforce “admin-only” access across authentication, routing, and key UI entry points to prevent non-admin Google accounts from creating Firestore profiles.
Changes:
- Introduces a shared
ADMIN_EMAILconstant and replaces prior hardcoded admin email checks across the app. - Adds admin gating to Google sign-in and strengthens routing/app-level session enforcement for non-admin users.
- Improves login UX with disabled state/spinner and a dedicated “access restricted” error message.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/app/services/auth.service.ts | Adds ADMIN_EMAIL constant and blocks non-admin sign-in in loginWithGoogle; adjusts new-profile defaults. |
| src/app/components/secrets-manager/secrets-manager.component.ts | Replaces hardcoded admin email check with ADMIN_EMAIL. |
| src/app/components/profile/profile.component.ts | Replaces hardcoded admin email checks with ADMIN_EMAIL and exposes constant for template usage. |
| src/app/components/onboarding/onboarding.component.ts | Replaces hardcoded admin email check with ADMIN_EMAIL in email verification helper. |
| src/app/components/dashboard/dashboard.component.ts | Replaces hardcoded admin email check with ADMIN_EMAIL. |
| src/app/components/auth/auth.component.ts | Adds in-flight login UX (disable/spinner) and renders ACCESS_DENIED banner messaging. |
| src/app/components/admin-portal/admin-portal.component.ts | Replaces hardcoded admin email check with ADMIN_EMAIL. |
| src/app/app.ts | Adds defense-in-depth: signs out and redirects if a non-admin session exists; adjusts verification gating. |
| src/app/app.routes.ts | Updates guards (authGuard, publicGuard, onboardingGuard) to explicitly require ADMIN_EMAIL. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (result.user.email !== ADMIN_EMAIL) { | ||
| // Sign the non-admin user out immediately so no session is created. | ||
| await signOut(auth); | ||
| throw new Error('ACCESS_DENIED'); | ||
| } | ||
|
|
||
| await this.loadUserProfile(result.user); | ||
| } |
There was a problem hiding this comment.
The non-admin restriction in loginWithGoogle can be bypassed by the onAuthStateChanged handler: as soon as signInWithPopup succeeds, the auth-state callback may run and call loadUserProfile(user), which can create a Firestore profile document before this method signs the user out. To prevent non-admin profile creation, add the same ADMIN_EMAIL gate in the auth-state listener (before calling loadUserProfile), or make loadUserProfile refuse/avoid creating documents for non-admin users.
There was a problem hiding this comment.
Fixed in commit 420a2a5. Three changes were made together:
-
onAuthStateChangedrace closed — the listener now checksADMIN_EMAILbefore callingloadUserProfile. If the user isn't the admin,signOutis called immediately and the signals are never updated, so no Firestore profile document can be created regardless of how quickly Firebase fires the state-change event. -
Implicit-any removed —
resultinloginWithGoogleis now typed asUserCredential. -
Email-verification bypass aligned —
app.tsandapp.routes.tsnow useuser.email === ADMIN_EMAIL || user.emailVerified(matching the existing bypass inonboarding.component.ts), so routing decisions are consistent across all three enforcement points.
|
@copilot apply changes based on the comments in this thread |
…ification bypass - onAuthStateChanged now gates on ADMIN_EMAIL before calling loadUserProfile, preventing any Firestore profile creation for non-admin accounts even during the brief window between signInWithPopup resolving and signOut completing - loginWithGoogle: type `result` as `UserCredential` (removes implicit-any) - app.ts + app.routes.ts: restore admin email-verification bypass (user.email === ADMIN_EMAIL || user.emailVerified) so routing/guards match the existing bypass in onboarding.component.ts isEmailVerified() Agent-Logs-Url: https://github.com/schoedel-learn/LXPython/sessions/70771064-591c-4c0b-aac2-c44c46f2a52b Co-authored-by: schoedel-learn <212212630+schoedel-learn@users.noreply.github.com>
Replace isOwner(userId)/isAuthenticated() with isAdmin() everywhere so
non-admin accounts cannot create, read, update, or delete any Firestore
data even when bypassing the client-side enforcement.
- /users/{userId} and all sub-collections (secrets, progress, attempts,
memories, chats): require isAdmin() on every operation
- /forumPosts and /replies: require isAdmin() on every operation,
removing the redundant multi-user moderation branches
- Add comment to isAdmin() noting it must stay in sync with ADMIN_EMAIL
Agent-Logs-Url: https://github.com/schoedel-learn/LXPython/sessions/a8d49aa4-b999-4a05-8045-71dd05ca71e4
Co-authored-by: schoedel-learn <212212630+schoedel-learn@users.noreply.github.com>
All items from the review thread are now addressed across commits
|
The app had no mechanism to prevent any Google account from signing in and creating a Firestore profile. The admin email was also hardcoded as a string literal in 9 separate places.
Changes
Single source of truth for admin identity
ADMIN_EMAILconstant fromauth.service.ts; replaced all 9 inline'schoedelb@gmail.com'string literals acrossdashboard,onboarding,profile,admin-portal, andsecrets-managerEnforcement at the auth layer (primary gate)
loginWithGooglenow rejects non-admins before any profile data is written:The
onAuthStateChangedlistener also gates onADMIN_EMAILbefore callingloadUserProfile, closing the race window where a non-admin profile could be created betweensignInWithPopupresolving and the explicitsignOutcompleting.Defence-in-depth layers
app.tsauth-state effect: signs out + redirects any non-admin user that somehow holds an active session; usesuser.email === ADMIN_EMAIL || user.emailVerifiedto align email-verification logic withonboarding.component.tsapp.routes.tsall three guards (authGuard,publicGuard,onboardingGuard): explicitly gate onADMIN_EMAILand use the same consistent email-verification bypass for the admin accountfirestore.rules: replaced allisOwner(userId)/isAuthenticated()checks withisAdmin()across every collection (/users/{userId}and sub-collections,/forumPosts,/replies) — server-side enforcement so non-admins cannot create, read, update, or delete any data even via direct SDK callsUX feedback in
auth.component.tsACCESS_DENIEDrenders a "🔒 Access restricted" error banner; other Firebase errors show a generic retry message