Skip to content

Add teen delegated login feature#5

Merged
tpaulshippy merged 2 commits intomainfrom
feature/teen-delegated-login
Apr 12, 2026
Merged

Add teen delegated login feature#5
tpaulshippy merged 2 commits intomainfrom
feature/teen-delegated-login

Conversation

@tpaulshippy
Copy link
Copy Markdown
Owner

@tpaulshippy tpaulshippy commented Apr 12, 2026

Summary

  • Adds oauth_email field to Profile model for delegated teen login
  • When teen logs in with their OAuth email, backend returns parent's JWT tokens with teen's profile pre-selected
  • Configure via Django admin at /admin/bots/profile/ - edit a profile to add the teen's OAuth email

Implementation Details

  • Added oauth_email field to Profile model (nullable, blankable)
  • Modified get_jwt view to check if user.email matches any Profile's oauth_email; if so, returns parent's JWT tokens with active_profile_id set to that profile
  • No React UI changes required - existing login flow works unchanged

Usage

  1. Create a teen profile under parent account (or use existing)
  2. Edit the profile in admin and set oauth_email to the teen's Google/Apple email
  3. When teen logs in, they get parent's account with their profile active

Testing

  • Create a profile under a parent account
  • Set oauth_email to a test email address
  • Log in with that email → verify you get parent's tokens with teen's profile

- Add TeenEmailMapping model to map teen OAuth email to parent profiles
- Add admin UI for configuring teen email mappings via Django admin
- Modify JWT generation to return parent's tokens + teen's profile when teen logs in
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

A new TeenEmailMapping model is introduced to enable parent account delegation of authentication tokens to teen profiles. The feature includes a database model with relationships to user accounts and profiles, a migration, Django admin interface configuration, model exports, and conditional JWT token generation based on OAuth email mapping lookup.

Changes

Cohort / File(s) Summary
Model & Database
back/bots/models/teen_email_mapping.py, back/bots/migrations/0034_teenemailmapping.py, back/bots/models/__init__.py
New TeenEmailMapping model created with OneToOne link to Profile, ForeignKey to UserAccount (parent), oauth_email field, and timestamps. Composite uniqueness enforced on (oauth_email, parent_account). Model exported from package init.
Admin Interface
back/bots/admin.py
New TeenEmailMappingAdmin class registered with Django admin site, exposing oauth_email, teen_profile, parent_account, and timestamps in changelist, with creation/modification timestamps as read-only.
JWT Token Generation
back/bots/views/get_jwt.py
JWT endpoint modified to conditionally issue delegated tokens. Looks up TeenEmailMapping by oauth_email; if found, generates tokens for parent account via new get_delegated_tokens() helper; otherwise falls back to standard token generation. Response includes active_profile_id and is_teen_delegated flags.

Sequence Diagram

sequenceDiagram
    participant Client
    participant JWT Endpoint
    participant TeenEmailMapping DB
    participant Token Generator
    participant Client Response

    Client->>JWT Endpoint: POST /get_jwt (oauth_email=user@teen.com)
    JWT Endpoint->>TeenEmailMapping DB: lookup(oauth_email, select_related)
    alt Mapping Found
        TeenEmailMapping DB-->>JWT Endpoint: TeenEmailMapping object
        JWT Endpoint->>Token Generator: get_delegated_tokens(parent_user, teen_profile)
        Token Generator-->>JWT Endpoint: access, refresh tokens
        JWT Endpoint->>Client Response: {access, refresh, active_profile_id, is_teen_delegated=true}
    else No Mapping
        TeenEmailMapping DB-->>JWT Endpoint: None
        JWT Endpoint->>Token Generator: generate_tokens(authenticated_user)
        Token Generator-->>JWT Endpoint: access, refresh tokens
        JWT Endpoint->>Client Response: {access, refresh, is_teen_delegated=false}
    end
    Client Response-->>Client: JWT tokens
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A mapping emerges, oauth to teen,
Where parent and child share tokens unseen,
Delegation flows smooth through the JWT gate,
Teen profiles flourish—their access is great!
Hop along, code—let delegated trust reign! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Add teen delegated login feature' clearly and concisely summarizes the main change: introducing a teen delegated login capability where teens can log in and access parent accounts with their profile pre-selected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/teen-delegated-login

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
back/bots/admin.py (1)

68-74: Consider list_select_related to avoid admin N+1 queries.

get_list_display shows teen_profile and parent_account; adding list_select_related will reduce query count on large mapping tables.

♻️ Suggested refactor
 class TeenEmailMappingAdmin(admin.ModelAdmin):
+    list_select_related = ('teen_profile', 'parent_account')
+
     def get_readonly_fields(self, request, obj=None):
         return ['created_at', 'modified_at']
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@back/bots/admin.py` around lines 68 - 74, Add eager loading to the
TeenEmailMappingAdmin to avoid admin N+1 queries: in the TeenEmailMappingAdmin
class (where get_list_display is defined) add a list_select_related attribute or
override get_queryset to call select_related for the related fields
'teen_profile' and 'parent_account' so the Django admin fetches those relations
in one query instead of issuing per-row queries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@back/bots/models/teen_email_mapping.py`:
- Around line 7-20: Add a UUID public identifier field to the TeenEmailMapping
model: define a field named teen_email_mapping_id as
models.UUIDField(default=uuid.uuid4, editable=False, unique=True, db_index=True)
on the TeenEmailMapping class, import uuid at the top of the file, and create a
migration so the new column is added; ensure the field follows the same naming
and uniqueness pattern used by other models (e.g., profile_id, bot_id) and is
placed alongside the existing model fields.
- Around line 13-24: The TeenEmailMapping model allows mismatched teen_profile
and parent_account; add model-level validation by implementing
TeenEmailMapping.clean() to raise django.core.exceptions.ValidationError when
self.teen_profile.user != self.parent_account.user (or equivalent user
relationship), and ensure this runs on save by calling full_clean() in
TeenEmailMapping.save() (or configure the admin ModelAdmin to call
full_clean()/validate on save_model) so the admin UI and programmatic saves
cannot persist inconsistent mappings; keep the existing unique_together intact.

In `@back/bots/views/get_jwt.py`:
- Around line 16-25: Update get_delegated_tokens to accept the full mapping
object (e.g., rename parameter to mapping) instead of teen_profile, validate
that teen_profile.user equals mapping.parent_account before issuing tokens, and
raise a custom exception (e.g., DelegationMappingError) if the mapped
parent_account and teen_profile.user do not match; then use
mapping.parent_account when creating the RefreshToken
(RefreshToken.for_user(mapping.parent_account)) and update the caller to pass
the mapping object into get_delegated_tokens. Ensure the new exception is
defined and used by the existing error handling path so validation failures are
caught alongside missing mappings.
- Around line 41-46: The TeenEmailMapping lookup uses .get() on a non-unique
field and can raise MultipleObjectsReturned and also performs a case-sensitive
match; update the lookup in the delegated login path (the block that currently
calls TeenEmailMapping.objects.select_related(...).get(oauth_email=user.email)
and then get_delegated_tokens(user, mapping.teen_profile)) to perform a
case-insensitive lookup using oauth_email__iexact, handle
MultipleObjectsReturned by deterministically selecting one mapping (for example
the first mapping ordered by id or parent_account) or return a controlled error,
and fall back to the existing RefreshToken.for_user(user) path when no mapping
is found; ensure you also catch django.core.exceptions.MultipleObjectsReturned
around the original .get() if you prefer to keep it and add explicit handling
there.

---

Nitpick comments:
In `@back/bots/admin.py`:
- Around line 68-74: Add eager loading to the TeenEmailMappingAdmin to avoid
admin N+1 queries: in the TeenEmailMappingAdmin class (where get_list_display is
defined) add a list_select_related attribute or override get_queryset to call
select_related for the related fields 'teen_profile' and 'parent_account' so the
Django admin fetches those relations in one query instead of issuing per-row
queries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e85fb515-0bad-4422-805c-f088614337a9

📥 Commits

Reviewing files that changed from the base of the PR and between ffd09f3 and bff6dd5.

📒 Files selected for processing (5)
  • back/bots/admin.py
  • back/bots/migrations/0034_teenemailmapping.py
  • back/bots/models/__init__.py
  • back/bots/models/teen_email_mapping.py
  • back/bots/views/get_jwt.py

Comment on lines +7 to +20
class TeenEmailMapping(models.Model):
teen_profile = models.OneToOneField(
Profile,
on_delete=models.CASCADE,
related_name='teen_email_mapping'
)
parent_account = models.ForeignKey(
UserAccount,
on_delete=models.CASCADE,
related_name='teen_mappings'
)
oauth_email = models.EmailField(max_length=254)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and examine its complete contents
cat -n back/bots/models/teen_email_mapping.py 2>/dev/null || echo "File not found or error reading"

Repository: tpaulshippy/bots

Length of output: 1017


🏁 Script executed:

# List all model files in the bots/models directory to understand the structure
ls -la back/bots/models/*.py 2>/dev/null | head -20

Repository: tpaulshippy/bots

Length of output: 857


🏁 Script executed:

# Search for UUID field usage patterns in existing models
rg "UUIDField" back/bots/models/ -A 2 -B 2

Repository: tpaulshippy/bots

Length of output: 1996


🏁 Script executed:

# Check for uuid imports in model files
rg "import uuid" back/bots/models/

Repository: tpaulshippy/bots

Length of output: 251


🏁 Script executed:

# Verify the exact import and field pattern in one of the existing models
head -15 back/bots/models/bot.py

Repository: tpaulshippy/bots

Length of output: 413


🏁 Script executed:

# Check if teen_email_mapping_id follows the naming convention used elsewhere
rg "_id = models.UUIDField" back/bots/models/ | head -10

Repository: tpaulshippy/bots

Length of output: 598


Add a UUID public identifier field to this model.

TeenEmailMapping is missing the {model_name}_id UUID field required by the model guidelines. All other models in back/bots/models/ implement this pattern: profile_id, bot_id, chat_id, device_id, and message_id.

♻️ Suggested change
+import uuid
 from django.conf import settings
 from django.db import models
 from .profile import Profile
 from .user_account import UserAccount
 
 
 class TeenEmailMapping(models.Model):
+    teen_email_mapping_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
     teen_profile = models.OneToOneField(
         Profile,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@back/bots/models/teen_email_mapping.py` around lines 7 - 20, Add a UUID
public identifier field to the TeenEmailMapping model: define a field named
teen_email_mapping_id as models.UUIDField(default=uuid.uuid4, editable=False,
unique=True, db_index=True) on the TeenEmailMapping class, import uuid at the
top of the file, and create a migration so the new column is added; ensure the
field follows the same naming and uniqueness pattern used by other models (e.g.,
profile_id, bot_id) and is placed alongside the existing model fields.

Comment on lines +41 to +46
mapping = TeenEmailMapping.objects.select_related('teen_profile', 'parent_account').get(
oauth_email=user.email
)
response_data = get_delegated_tokens(user, mapping.teen_profile)
except TeenEmailMapping.DoesNotExist:
refresh = RefreshToken.for_user(user)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the TeenEmailMapping model definition
find . -type f -name "*.py" | xargs grep -l "class TeenEmailMapping" | head -5

Repository: tpaulshippy/bots

Length of output: 121


🏁 Script executed:

# Locate the get_jwt.py file
find . -type f -name "get_jwt.py" | head -5

Repository: tpaulshippy/bots

Length of output: 88


🏁 Script executed:

# Search for unique constraints on TeenEmailMapping
rg "TeenEmailMapping" -A 20 -B 2 | head -100

Repository: tpaulshippy/bots

Length of output: 5832


🏁 Script executed:

cat -n back/bots/views/get_jwt.py

Repository: tpaulshippy/bots

Length of output: 2266


🏁 Script executed:

cat -n back/bots/models/teen_email_mapping.py

Repository: tpaulshippy/bots

Length of output: 1017


🏁 Script executed:

# Check if there's parent_account context available from request/auth
rg "parent_account" back/bots/views/ -B 3 -A 3

Repository: tpaulshippy/bots

Length of output: 497


🏁 Script executed:

# Check error handling patterns in the codebase
rg "MultipleObjectsReturned" back/bots/ -B 2 -A 2

Repository: tpaulshippy/bots

Length of output: 42


Add exception handling for ambiguous email mappings in delegated login path.

Line 41–43 queries TeenEmailMapping.objects.get(oauth_email=user.email) on a non-unique field. The model permits multiple records per oauth_email across different parent_account values (via unique_together). When duplicates exist, .get() raises MultipleObjectsReturned, which is not caught, causing a 500 error during login.

Add explicit handling for this case and use case-insensitive email matching:

Suggested fix
     try:
-        mapping = TeenEmailMapping.objects.select_related('teen_profile', 'parent_account').get(
-            oauth_email=user.email
-        )
+        mapping = TeenEmailMapping.objects.select_related('teen_profile', 'parent_account').get(
+            oauth_email__iexact=user.email
+        )
         response_data = get_delegated_tokens(user, mapping.teen_profile)
     except TeenEmailMapping.DoesNotExist:
         refresh = RefreshToken.for_user(user)
         response_data = {
             'access': str(refresh.access_token),
             'refresh': str(refresh),
         }
+    except TeenEmailMapping.MultipleObjectsReturned:
+        return JsonResponse(
+            {'detail': 'Multiple teen delegated mappings found for this OAuth email.'},
+            status=409
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mapping = TeenEmailMapping.objects.select_related('teen_profile', 'parent_account').get(
oauth_email=user.email
)
response_data = get_delegated_tokens(user, mapping.teen_profile)
except TeenEmailMapping.DoesNotExist:
refresh = RefreshToken.for_user(user)
mapping = TeenEmailMapping.objects.select_related('teen_profile', 'parent_account').get(
oauth_email__iexact=user.email
)
response_data = get_delegated_tokens(user, mapping.teen_profile)
except TeenEmailMapping.DoesNotExist:
refresh = RefreshToken.for_user(user)
except TeenEmailMapping.MultipleObjectsReturned:
return JsonResponse(
{'detail': 'Multiple teen delegated mappings found for this OAuth email.'},
status=409
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@back/bots/views/get_jwt.py` around lines 41 - 46, The TeenEmailMapping lookup
uses .get() on a non-unique field and can raise MultipleObjectsReturned and also
performs a case-sensitive match; update the lookup in the delegated login path
(the block that currently calls
TeenEmailMapping.objects.select_related(...).get(oauth_email=user.email) and
then get_delegated_tokens(user, mapping.teen_profile)) to perform a
case-insensitive lookup using oauth_email__iexact, handle
MultipleObjectsReturned by deterministically selecting one mapping (for example
the first mapping ordered by id or parent_account) or return a controlled error,
and fall back to the existing RefreshToken.for_user(user) path when no mapping
is found; ensure you also catch django.core.exceptions.MultipleObjectsReturned
around the original .get() if you prefer to keep it and add explicit handling
there.

- Add oauth_email field to Profile model
- Simplify get_jwt to lookup Profile by oauth_email
- Remove TeenEmailMapping model and admin
- Update Profile admin to show oauth_email in list view
@tpaulshippy tpaulshippy merged commit b370b05 into main Apr 12, 2026
4 checks passed
@tpaulshippy tpaulshippy deleted the feature/teen-delegated-login branch April 12, 2026 02:58
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.

1 participant