Compare commits
3 Commits
219c22c679
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| beca854176 | |||
| ac70da42f4 | |||
| adf54f2632 |
@@ -0,0 +1,45 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2E tests for user profile management:
|
||||||
|
* - Update name/email via PUT /auth/me
|
||||||
|
* - Delete (deactivate) account via DELETE /auth/me
|
||||||
|
*/
|
||||||
|
|
||||||
|
test.describe('User Profile Management', () => {
|
||||||
|
const email = `test-${Date.now()}@example.com`;
|
||||||
|
const password = 'TestPass123!';
|
||||||
|
|
||||||
|
test.beforeAll(async ({ page }) => {
|
||||||
|
// Register a new user first
|
||||||
|
await page.goto('/register');
|
||||||
|
await page.fill('input[placeholder="Full name"]', 'John Doe');
|
||||||
|
await page.fill('input[placeholder="name@example.com"]', email);
|
||||||
|
await page.fill('input[placeholder="Password"]', password);
|
||||||
|
await page.click('button:has-text("Sign up")');
|
||||||
|
await expect(page).toHaveURL('/')
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update profile information', async ({ page }) => {
|
||||||
|
// Open settings profile page
|
||||||
|
await page.goto('/settings/profile');
|
||||||
|
await page.fill('input[name="first_name"]', 'Jane');
|
||||||
|
await page.fill('input[name="last_name"]', 'Smith');
|
||||||
|
await page.fill('input[name="email"]', `jane.${Date.now()}@example.com`);
|
||||||
|
await page.click('button:has-text("Save Changes")');
|
||||||
|
await expect(page.locator('p')).toContainText('Jane Smith');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete account (soft‑delete)', async ({ page }) => {
|
||||||
|
await page.goto('/settings/account');
|
||||||
|
await page.click('button:has-text("Delete Account")');
|
||||||
|
// Confirm modal (Playwright handles native confirm)
|
||||||
|
page.on('dialog', dialog => dialog.accept());
|
||||||
|
await expect(page).toHaveURL('/login');
|
||||||
|
// Verify login fails after deletion
|
||||||
|
await page.fill('input[placeholder="name@example.com"]', email);
|
||||||
|
await page.fill('input[placeholder="Password"]', password);
|
||||||
|
await page.click('button:has-text("Sign in")');
|
||||||
|
await expect(page.locator('div[role="alert"]')).toContainText('Invalid email or password');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# 🚀 Fase 3: Backend Enhancement & Testing
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Dopo il completamento di Fase 1 (Forgot Password) e Fase 2 (User Profile), questa fase si concentra sul backend e i test.
|
||||||
|
|
||||||
|
## Obiettivi Principali
|
||||||
|
|
||||||
|
### 1. Backend: PUT /auth/me
|
||||||
|
- **Endpoint**: `PUT /api/v1/auth/me`
|
||||||
|
- **Funzionalità**: Aggiornare nome, cognome, email utente
|
||||||
|
- **Validazione**: Pydantic schema con `UserUpdate`
|
||||||
|
- **Deadline**: 2026-04-14
|
||||||
|
|
||||||
|
### 2. Backend: DELETE /auth/me
|
||||||
|
- **Endpoint**: `DELETE /api/v1/auth/me`
|
||||||
|
- **Funzionalità**: Disattivare account utente (soft delete)
|
||||||
|
- **Dipendenze**: Revocare API keys associate
|
||||||
|
- **Deadline**: 2026-04-15
|
||||||
|
|
||||||
|
### 3. Frontend Integration
|
||||||
|
- **SettingsProfile.tsx**: Form aggiornamento profilo
|
||||||
|
- **SettingsAccount.tsx**: Pulsante disattiva account
|
||||||
|
- **useProfile.ts**: Hook per nuove API
|
||||||
|
|
||||||
|
### 4. Test E2E
|
||||||
|
- Test profilo utente (update nome/cognome)
|
||||||
|
- Test cambio password
|
||||||
|
- Test disattivazione account
|
||||||
|
- Test autenticazione (login/logout)
|
||||||
|
|
||||||
|
## Stack Tecnologico
|
||||||
|
- **Backend**: FastAPI + SQLAlchemy + PostgreSQL
|
||||||
|
- **Frontend**: React + TypeScript + Tailwind
|
||||||
|
- **Testing**: Playwright
|
||||||
|
|
||||||
|
## Riferimenti
|
||||||
|
- Schema: `src/schemas/user.py`
|
||||||
|
- API: `src/api/v1/auth.py`
|
||||||
|
- Frontend: `frontend/src/pages/settings/`
|
||||||
|
|
||||||
|
## Team Assignment
|
||||||
|
- @backend-dev: Endpoints PUT/DELETE
|
||||||
|
- @frontend-dev: Integrazione UI
|
||||||
|
- @qa-engineer: E2E tests
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- [ ] PUT /auth/me funziona con validazione
|
||||||
|
- [ ] DELETE /auth/me disattiva account
|
||||||
|
- [ ] Frontend aggiornato con nuovi form
|
||||||
|
- [ ] Test E2E passano
|
||||||
|
- [ ] Build successful
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Prompt generato: 2026-04-08*
|
||||||
|
*Status: Pronto per assegnazione team*
|
||||||
@@ -246,6 +246,74 @@ async def get_me(
|
|||||||
"""
|
"""
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
# ---------------------------------------------------
|
||||||
|
# Update profile (PUT /auth/me)
|
||||||
|
# ---------------------------------------------------
|
||||||
|
@router.put(
|
||||||
|
"/me",
|
||||||
|
response_model=UserResponse,
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def update_me(
|
||||||
|
update_data: UserUpdate,
|
||||||
|
current_user: Annotated[UserResponse, Depends(get_current_user)],
|
||||||
|
session: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Update current user profile (first_name, last_name, email)."""
|
||||||
|
# fetch full user record
|
||||||
|
from uuid import UUID
|
||||||
|
user = await get_user_by_id(session, UUID(current_user.id))
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
if update_data.first_name is not None:
|
||||||
|
user.first_name = update_data.first_name
|
||||||
|
if update_data.last_name is not None:
|
||||||
|
user.last_name = update_data.last_name
|
||||||
|
if update_data.email is not None:
|
||||||
|
# ensure email is unique
|
||||||
|
from sqlalchemy import select
|
||||||
|
result = await session.execute(select(User).where(User.email == update_data.email, User.id != user.id))
|
||||||
|
if result.scalar_one_or_none():
|
||||||
|
raise HTTPException(status_code=400, detail="Email already in use")
|
||||||
|
user.email = update_data.email
|
||||||
|
|
||||||
|
session.add(user)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(user)
|
||||||
|
return UserResponse.model_validate(user)
|
||||||
|
|
||||||
|
# ---------------------------------------------------
|
||||||
|
# Delete account (DELETE /auth/me)
|
||||||
|
# ---------------------------------------------------
|
||||||
|
@router.delete(
|
||||||
|
"/me",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def delete_me(
|
||||||
|
current_user: Annotated[UserResponse, Depends(get_current_user)],
|
||||||
|
session: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Soft‑delete (deactivate) the current user account and revoke API keys."""
|
||||||
|
from uuid import UUID
|
||||||
|
user = await get_user_by_id(session, UUID(current_user.id))
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
# deactivate user
|
||||||
|
user.is_active = False
|
||||||
|
session.add(user)
|
||||||
|
# revoke all API keys (if any)
|
||||||
|
try:
|
||||||
|
from src.models.api_key import APIKey
|
||||||
|
from sqlalchemy import update
|
||||||
|
await session.execute(update(APIKey).where(APIKey.user_id == user.id).values(active=False))
|
||||||
|
except Exception:
|
||||||
|
pass # ignore if APIKey model not present
|
||||||
|
await session.commit()
|
||||||
|
return {"message": "Account deactivated successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/change-password",
|
"/change-password",
|
||||||
|
|||||||
+2
-1
@@ -16,7 +16,8 @@ class User(Base, TimestampMixin):
|
|||||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
email = Column(String(255), nullable=False, unique=True)
|
email = Column(String(255), nullable=False, unique=True)
|
||||||
password_hash = Column(String(255), nullable=False)
|
password_hash = Column(String(255), nullable=False)
|
||||||
full_name = Column(String(255), nullable=True)
|
first_name = Column(String(255), nullable=True)
|
||||||
|
last_name = Column(String(255), nullable=True)
|
||||||
is_active = Column(Boolean, default=True, nullable=False)
|
is_active = Column(Boolean, default=True, nullable=False)
|
||||||
is_superuser = Column(Boolean, default=False, nullable=False)
|
is_superuser = Column(Boolean, default=False, nullable=False)
|
||||||
last_login = Column(DateTime(timezone=True), nullable=True)
|
last_login = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|||||||
+5
-2
@@ -10,7 +10,8 @@ class UserBase(BaseModel):
|
|||||||
"""Base user schema."""
|
"""Base user schema."""
|
||||||
|
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
full_name: Optional[str] = Field(None, max_length=255)
|
first_name: Optional[str] = Field(None, max_length=255)
|
||||||
|
last_name: Optional[str] = Field(None, max_length=255)
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(UserBase):
|
class UserCreate(UserBase):
|
||||||
@@ -22,7 +23,9 @@ class UserCreate(UserBase):
|
|||||||
class UserUpdate(BaseModel):
|
class UserUpdate(BaseModel):
|
||||||
"""Schema for updating a user."""
|
"""Schema for updating a user."""
|
||||||
|
|
||||||
full_name: Optional[str] = Field(None, max_length=255)
|
first_name: Optional[str] = Field(None, max_length=255)
|
||||||
|
last_name: Optional[str] = Field(None, max_length=255)
|
||||||
|
email: Optional[EmailStr] = Field(None, max_length=255)
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(UserBase):
|
class UserResponse(UserBase):
|
||||||
|
|||||||
@@ -536,15 +536,17 @@ Il sistema può inviare email ma l'utente non ha visibilità sullo stato.
|
|||||||
- [x] Build verificato
|
- [x] Build verificato
|
||||||
- [ ] Test end-to-end (da fare)
|
- [ ] Test end-to-end (da fare)
|
||||||
|
|
||||||
- [ ] **Fase 2: User Profile**
|
- [x] **Fase 2: User Profile** ✅ COMPLETATO (2026-04-12)
|
||||||
- [ ] Profile.tsx
|
- [x] Profile.tsx
|
||||||
- [ ] SettingsLayout.tsx
|
- [x] SettingsLayout.tsx
|
||||||
- [ ] SettingsProfile.tsx
|
- [x] SettingsProfile.tsx
|
||||||
- [ ] SettingsPassword.tsx
|
- [x] SettingsPassword.tsx
|
||||||
- [ ] Header dropdown menu
|
- [x] SettingsNotifications.tsx
|
||||||
- [ ] Routes protette
|
- [x] SettingsAccount.tsx
|
||||||
- [ ] Hook useProfile
|
- [x] Header dropdown menu
|
||||||
- [ ] Test funzionalità
|
- [x] Routes protette
|
||||||
|
- [x] Hook useProfile
|
||||||
|
- [x] Test funzionalità
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user