Skip to content

Commit 4e97130

Browse files
m4dm4rtig4nClément VALENTINclaude
authored
feat: make demo account read-only (#83)
Implement read-only mode for the demo account ([email protected]) by adding a centralized middleware dependency that blocks all write operations. Changes: - Add `require_not_demo` middleware dependency to block demo account from write operations - Add `is_demo_user()` helper function and `DEMO_EMAIL` constant - Apply `require_not_demo` to all write endpoints in: - PDL management (create, delete, update operations) - Energy contributions (create, update, reply) - Account operations (delete, regenerate secret, update password) The demo account can still perform all read operations (GET endpoints) but will receive HTTP 403 with message "Le compte de démonstration est en lecture seule" when attempting write operations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Clément VALENTIN <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 99f3131 commit 4e97130

File tree

5 files changed

+50
-21
lines changed

5 files changed

+50
-21
lines changed
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
from .auth import get_current_user
1+
from .auth import get_current_user, require_not_demo, is_demo_user, DEMO_EMAIL
22
from .admin import require_admin, require_permission, require_action
33

4-
__all__ = ["get_current_user", "require_admin", "require_permission", "require_action"]
4+
__all__ = [
5+
"get_current_user",
6+
"require_admin",
7+
"require_permission",
8+
"require_action",
9+
"require_not_demo",
10+
"is_demo_user",
11+
"DEMO_EMAIL",
12+
]

apps/api/src/middleware/auth.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
logger = logging.getLogger(__name__)
1717

18+
# Demo account email constant
19+
DEMO_EMAIL = "[email protected]"
20+
1821
oauth2_scheme = OAuth2(
1922
flows=OAuthFlowsModel(
2023
clientCredentials={
@@ -133,3 +136,21 @@ async def get_current_user_optional(
133136
return user
134137

135138
return None
139+
140+
141+
def is_demo_user(user: User) -> bool:
142+
"""Check if the user is a demo account"""
143+
return user.email == DEMO_EMAIL
144+
145+
146+
async def require_not_demo(current_user: User = Depends(get_current_user)) -> User:
147+
"""
148+
Middleware that blocks demo accounts from performing write operations.
149+
Use this dependency on any endpoint that modifies data.
150+
"""
151+
if is_demo_user(current_user):
152+
raise HTTPException(
153+
status_code=status.HTTP_403_FORBIDDEN,
154+
detail="Le compte de démonstration est en lecture seule"
155+
)
156+
return current_user

apps/api/src/routers/accounts.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sqlalchemy.orm import selectinload
1010

1111
from ..config import settings
12-
from ..middleware import get_current_user
12+
from ..middleware import get_current_user, require_not_demo
1313
from ..models import User, PDL, Token, EmailVerificationToken, PasswordResetToken, Role
1414
from ..models.database import get_db
1515
from ..schemas import (
@@ -314,7 +314,7 @@ async def get_credentials(current_user: User = Depends(get_current_user)) -> API
314314

315315
@router.delete("/me", response_model=APIResponse)
316316
async def delete_account(
317-
current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)
317+
current_user: User = Depends(require_not_demo), db: AsyncSession = Depends(get_db)
318318
) -> APIResponse:
319319
"""Delete user account and all associated data"""
320320
# Delete cache for all user's PDLs
@@ -436,7 +436,7 @@ async def resend_verification(
436436

437437
@router.post("/regenerate-secret", response_model=APIResponse)
438438
async def regenerate_secret(
439-
current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)
439+
current_user: User = Depends(require_not_demo), db: AsyncSession = Depends(get_db)
440440
) -> APIResponse:
441441
"""Regenerate client_secret and clear all cache"""
442442
# Generate new client_secret
@@ -589,7 +589,7 @@ async def reset_password(request: Request, db: AsyncSession = Depends(get_db)) -
589589
@router.post("/update-password", response_model=APIResponse)
590590
async def update_password(
591591
request: Request,
592-
current_user: User = Depends(get_current_user),
592+
current_user: User = Depends(require_not_demo),
593593
db: AsyncSession = Depends(get_db)
594594
) -> APIResponse:
595595
"""Update password for authenticated user (requires old password verification)"""

apps/api/src/routers/energy_offers.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from ..models import User, EnergyProvider, EnergyOffer, OfferContribution, ContributionMessage
66
from ..models.database import get_db
77
from ..schemas import APIResponse, ErrorDetail
8-
from ..middleware import get_current_user, require_permission, require_action
8+
from ..middleware import get_current_user, require_permission, require_action, require_not_demo
99
from ..services.email import email_service
1010
from ..config import settings
1111
import logging
@@ -112,7 +112,7 @@ async def list_offers(
112112
# Contribution endpoints
113113
@router.post("/contribute", response_model=APIResponse, status_code=status.HTTP_201_CREATED)
114114
async def create_contribution(
115-
contribution_data: dict, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)
115+
contribution_data: dict, current_user: User = Depends(require_not_demo), db: AsyncSession = Depends(get_db)
116116
) -> APIResponse:
117117
"""Submit a new contribution for review"""
118118
logger.info(f"[CONTRIBUTION] New contribution from user: {current_user.email}")
@@ -168,7 +168,7 @@ async def create_contribution(
168168
async def update_contribution(
169169
contribution_id: str = Path(..., description="Contribution ID"),
170170
contribution_data: dict = Body(...),
171-
current_user: User = Depends(get_current_user),
171+
current_user: User = Depends(require_not_demo),
172172
db: AsyncSession = Depends(get_db),
173173
) -> APIResponse:
174174
"""Update an existing contribution (only pending or rejected ones owned by the user)"""
@@ -303,7 +303,7 @@ async def list_my_contributions(current_user: User = Depends(get_current_user),
303303
async def reply_to_contribution(
304304
contribution_id: str,
305305
body: dict = Body(...),
306-
current_user: User = Depends(get_current_user),
306+
current_user: User = Depends(require_not_demo),
307307
db: AsyncSession = Depends(get_db),
308308
) -> APIResponse:
309309
"""Allow contributor to reply to admin messages on their own contribution"""

apps/api/src/routers/pdl.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ..models.database import get_db
88
from ..schemas import PDLCreate, PDLResponse, APIResponse, ErrorDetail
99
from ..schemas.requests import AdminPDLCreate
10-
from ..middleware import get_current_user, require_admin, require_permission
10+
from ..middleware import get_current_user, require_admin, require_permission, require_not_demo
1111
from ..routers.enedis import get_valid_token
1212
from ..adapters import enedis_adapter
1313
import logging
@@ -129,7 +129,7 @@ async def create_pdl(
129129
}
130130
}
131131
),
132-
current_user: User = Depends(get_current_user),
132+
current_user: User = Depends(require_not_demo),
133133
db: AsyncSession = Depends(get_db)
134134
) -> APIResponse:
135135
"""Add a new PDL to current user"""
@@ -364,7 +364,7 @@ async def get_pdl(
364364
@router.delete("/{pdl_id}", response_model=APIResponse)
365365
async def delete_pdl(
366366
pdl_id: str = Path(..., description="PDL ID (UUID)", openapi_examples={"example_uuid": {"summary": "Example UUID", "value": "550e8400-e29b-41d4-a716-446655440000"}}),
367-
current_user: User = Depends(get_current_user),
367+
current_user: User = Depends(require_not_demo),
368368
db: AsyncSession = Depends(get_db)
369369
) -> APIResponse:
370370
"""Delete a PDL"""
@@ -384,7 +384,7 @@ async def delete_pdl(
384384
async def update_pdl_name(
385385
pdl_id: str = Path(..., description="PDL ID (UUID)", openapi_examples={"example_uuid": {"summary": "Example UUID", "value": "550e8400-e29b-41d4-a716-446655440000"}}),
386386
name_data: PDLUpdateName = Body(..., openapi_examples={"update_name": {"summary": "Update name", "value": {"name": "Nouveau nom de compteur"}}}),
387-
current_user: User = Depends(get_current_user),
387+
current_user: User = Depends(require_not_demo),
388388
db: AsyncSession = Depends(get_db),
389389
) -> APIResponse:
390390
"""Update PDL custom name"""
@@ -417,7 +417,7 @@ async def update_pdl_type(
417417
"production_only": {"summary": "Production only", "value": {"has_consumption": False, "has_production": True}},
418418
"both": {"summary": "Both consumption and production", "value": {"has_consumption": True, "has_production": True}}
419419
}),
420-
current_user: User = Depends(get_current_user),
420+
current_user: User = Depends(require_not_demo),
421421
db: AsyncSession = Depends(get_db),
422422
) -> APIResponse:
423423
"""Update PDL type (consumption and/or production)"""
@@ -451,7 +451,7 @@ async def toggle_pdl_active(
451451
"activate": {"summary": "Activate PDL", "value": {"is_active": True}},
452452
"deactivate": {"summary": "Deactivate PDL", "value": {"is_active": False}}
453453
}),
454-
current_user: User = Depends(get_current_user),
454+
current_user: User = Depends(require_not_demo),
455455
db: AsyncSession = Depends(get_db),
456456
) -> APIResponse:
457457
"""Toggle PDL active/inactive status"""
@@ -487,7 +487,7 @@ async def update_pdl_pricing_option(
487487
"hc_weekend": {"summary": "HC Nuit & Week-end", "value": {"pricing_option": "HC_WEEKEND"}},
488488
"clear": {"summary": "Remove pricing option", "value": {"pricing_option": None}}
489489
}),
490-
current_user: User = Depends(get_current_user),
490+
current_user: User = Depends(require_not_demo),
491491
db: AsyncSession = Depends(get_db),
492492
) -> APIResponse:
493493
"""
@@ -539,7 +539,7 @@ async def update_pdl_selected_offer(
539539
"select_offer": {"summary": "Select an energy offer", "value": {"selected_offer_id": "550e8400-e29b-41d4-a716-446655440001"}},
540540
"clear": {"summary": "Remove selected offer", "value": {"selected_offer_id": None}}
541541
}),
542-
current_user: User = Depends(get_current_user),
542+
current_user: User = Depends(require_not_demo),
543543
db: AsyncSession = Depends(get_db),
544544
) -> APIResponse:
545545
"""
@@ -608,7 +608,7 @@ async def link_production_pdl(
608608
"link": {"summary": "Link to production PDL", "value": {"linked_production_pdl_id": "550e8400-e29b-41d4-a716-446655440001"}},
609609
"unlink": {"summary": "Unlink production PDL", "value": {"linked_production_pdl_id": None}}
610610
}),
611-
current_user: User = Depends(get_current_user),
611+
current_user: User = Depends(require_not_demo),
612612
db: AsyncSession = Depends(get_db),
613613
) -> APIResponse:
614614
"""
@@ -723,7 +723,7 @@ async def update_pdl_contract(
723723
}
724724
}
725725
),
726-
current_user: User = Depends(get_current_user),
726+
current_user: User = Depends(require_not_demo),
727727
db: AsyncSession = Depends(get_db),
728728
) -> APIResponse:
729729
"""Update PDL contract information (subscribed power and offpeak hours)"""
@@ -770,7 +770,7 @@ async def reorder_pdls(
770770
}
771771
}
772772
),
773-
current_user: User = Depends(get_current_user),
773+
current_user: User = Depends(require_not_demo),
774774
db: AsyncSession = Depends(get_db),
775775
) -> APIResponse:
776776
"""Update display order for multiple PDLs"""

0 commit comments

Comments
 (0)