Skip to content

Restrict app access to admin account only#4

Merged
schoedel-learn merged 3 commits into
mainfrom
copilot/explore-codebase-and-create-plan
Apr 19, 2026
Merged

Restrict app access to admin account only#4
schoedel-learn merged 3 commits into
mainfrom
copilot/explore-codebase-and-create-plan

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 19, 2026

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

  • Exported ADMIN_EMAIL constant from auth.service.ts; replaced all 9 inline 'schoedelb@gmail.com' string literals across dashboard, onboarding, profile, admin-portal, and secrets-manager

Enforcement at the auth layer (primary gate)

loginWithGoogle now rejects non-admins before any profile data is written:

if (result.user.email !== ADMIN_EMAIL) {
  await signOut(auth);          // destroy Firebase session immediately
  throw new Error('ACCESS_DENIED');
}
await this.loadUserProfile(result.user);

The onAuthStateChanged listener also gates on ADMIN_EMAIL before calling loadUserProfile, closing the race window where a non-admin profile could be created between signInWithPopup resolving and the explicit signOut completing.

Defence-in-depth layers

  • app.ts auth-state effect: signs out + redirects any non-admin user that somehow holds an active session; uses user.email === ADMIN_EMAIL || user.emailVerified to align email-verification logic with onboarding.component.ts
  • app.routes.ts all three guards (authGuard, publicGuard, onboardingGuard): explicitly gate on ADMIN_EMAIL and use the same consistent email-verification bypass for the admin account
  • firestore.rules: replaced all isOwner(userId) / isAuthenticated() checks with isAdmin() 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 calls

UX feedback in auth.component.ts

  • Button disabled + spinner while the Google popup is in flight (prevents double-clicks)
  • ACCESS_DENIED renders a "🔒 Access restricted" error banner; other Firebase errors show a generic retry message

- 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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_EMAIL constant 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.

Comment on lines +59 to 66
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);
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 420a2a5. Three changes were made together:

  1. onAuthStateChanged race closed — the listener now checks ADMIN_EMAIL before calling loadUserProfile. If the user isn't the admin, signOut is 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.

  2. Implicit-any removedresult in loginWithGoogle is now typed as UserCredential.

  3. Email-verification bypass alignedapp.ts and app.routes.ts now use user.email === ADMIN_EMAIL || user.emailVerified (matching the existing bypass in onboarding.component.ts), so routing decisions are consistent across all three enforcement points.

Comment thread src/app/services/auth.service.ts
Comment thread src/app/components/onboarding/onboarding.component.ts
Comment thread src/app/app.ts Outdated
Comment thread src/app/app.routes.ts
Comment thread src/app/services/auth.service.ts
@schoedel-learn
Copy link
Copy Markdown
Owner

@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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 19, 2026

@copilot apply changes based on the comments in this thread

All items from the review thread are now addressed across commits 420a2a5 and 6b3a3cd:

  • onAuthStateChanged race — gated on ADMIN_EMAIL before loadUserProfile is called (420a2a5)
  • Implicit-anyresult typed as UserCredential (420a2a5)
  • Email-verification consistencyapp.ts and app.routes.ts restored the admin bypass (user.email === ADMIN_EMAIL || user.emailVerified) to match onboarding.component.ts (420a2a5)
  • Server-side Firestore enforcement (6b3a3cd) — firestore.rules now requires isAdmin() on every operation across all collections (/users/{userId} and sub-collections, /forumPosts and /replies). isOwner(userId) and plain isAuthenticated() are no longer accepted, so a non-admin account cannot write any data even via direct SDK calls.

@schoedel-learn schoedel-learn merged commit 33e3e7a into main Apr 19, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants