diff --git a/.github/specs/archive/feat-profile-mark-remove-account.md b/.github/specs/archive/feat-profile-mark-remove-account.md
new file mode 100644
index 0000000..eba9d37
--- /dev/null
+++ b/.github/specs/archive/feat-profile-mark-remove-account.md
@@ -0,0 +1,251 @@
+# Feature: Account Deletion (Mark for Removal)
+
+## Overview
+
+**Goal:** Allow users to mark their account for deletion from the Profile page.
+
+**User Story:**
+As a user, I want to delete my account from the Profile page. When I click "Delete My Account", I want a confirmation dialog that warns me about data loss. After confirming by entering my email, my account will be marked for deletion, I will be signed out, and I will not be able to log in again.
+
+---
+
+## Data Model Changes
+
+### Backend Model (`backend/models/user.py`)
+
+Add the following fields to the `User` class:
+
+```python
+marked_for_deletion: bool = False
+marked_for_deletion_at: datetime | None = None
+```
+
+- Update `to_dict()` and `from_dict()` methods to serialize these fields.
+- Import `datetime` from Python standard library if not already imported.
+
+### Frontend Model (`frontend/vue-app/src/common/models.ts`)
+
+Add matching fields to the `User` interface:
+
+```typescript
+marked_for_deletion: boolean;
+marked_for_deletion_at: string | null;
+```
+
+---
+
+## Backend Implementation
+
+### New Error Codes (`backend/api/error_codes.py`)
+
+Add the following error code:
+
+```python
+ACCOUNT_MARKED_FOR_DELETION = 'ACCOUNT_MARKED_FOR_DELETION'
+ALREADY_MARKED = 'ALREADY_MARKED'
+```
+
+### New API Endpoint (`backend/api/user_api.py`)
+
+**Endpoint:** `POST /api/user/mark-for-deletion`
+
+**Authentication:** Requires valid JWT (authenticated user).
+
+**Request:**
+
+```json
+{}
+```
+
+(Empty body; user is identified from JWT token)
+
+**Response:**
+
+- **Success (200):**
+ ```json
+ { "success": true }
+ ```
+- **Error (400/401/403):**
+ ```json
+ { "error": "Error message", "code": "INVALID_USER" | "ALREADY_MARKED" }
+ ```
+
+**Logic:**
+
+1. Extract current user from JWT token.
+2. Validate user exists in database.
+3. Check if already marked for deletion:
+ - If `marked_for_deletion == True`, return error with code `ALREADY_MARKED` (or make idempotent and return success).
+4. Set `marked_for_deletion = True` and `marked_for_deletion_at = datetime.now(timezone.utc)`.
+5. Save user to database using `users_db.update()`.
+6. Trigger SSE event: `send_event_for_current_user('user_marked_for_deletion', { 'user_id': user.id })`.
+7. Return success response.
+
+### Login Blocking (`backend/api/auth_api.py`)
+
+In the `/api/login` endpoint, after validating credentials:
+
+1. Check if `user.marked_for_deletion == True`.
+2. If yes, return:
+ ```json
+ {
+ "error": "This account has been marked for deletion and cannot be accessed.",
+ "code": "ACCOUNT_MARKED_FOR_DELETION"
+ }
+ ```
+ with HTTP status `403`.
+
+### Password Reset Blocking (`backend/api/user_api.py`)
+
+In the `/api/user/request-reset` endpoint:
+
+1. After finding the user by email, check if `user.marked_for_deletion == True`.
+2. If yes, **silently ignore the request**:
+ - Do not send an email.
+ - Return success response (to avoid leaking account status).
+
+### SSE Event (`backend/events/types/event_types.py`)
+
+Add new event type:
+
+```python
+USER_MARKED_FOR_DELETION = 'user_marked_for_deletion'
+```
+
+---
+
+## Frontend Implementation
+
+### Files Affected
+
+- `frontend/vue-app/src/components/parent/UserProfile.vue`
+- `frontend/vue-app/src/common/models.ts`
+- `frontend/vue-app/src/common/errorCodes.ts`
+
+### Error Codes (`frontend/vue-app/src/common/errorCodes.ts`)
+
+Add:
+
+```typescript
+export const ACCOUNT_MARKED_FOR_DELETION = "ACCOUNT_MARKED_FOR_DELETION";
+export const ALREADY_MARKED = "ALREADY_MARKED";
+```
+
+### UI Components (`UserProfile.vue`)
+
+#### 1. Delete Account Button
+
+- **Label:** "Delete My Account"
+- **Style:** `.btn-danger-link` (use `--danger` color from `colors.css`)
+- **Placement:** Below "Change Password" link, with `24px` margin-top
+- **Behavior:** Opens warning modal on click
+
+#### 2. Warning Modal (uses `ModalDialog.vue`)
+
+- **Title:** "Delete Your Account?"
+- **Body:**
+ "This will permanently delete your account and all associated data. This action cannot be undone."
+- **Email Confirmation Input:**
+ - Require user to type their email address to confirm.
+ - Display message: "Type your email address to confirm:"
+ - Input field with `v-model` bound to `confirmEmail` ref.
+- **Buttons:**
+ - **"Cancel"** (`.btn-secondary`) — closes modal
+ - **"Delete My Account"** (`.btn-danger`) — disabled until `confirmEmail` matches user email, triggers API call
+
+#### 3. Loading State
+
+- Disable "Delete My Account" button during API call.
+- Show loading spinner or "Deleting..." text.
+
+#### 4. Success Modal
+
+- **Title:** "Account Deleted"
+- **Body:**
+ "Your account has been marked for deletion. You will now be signed out."
+- **Button:** "OK" (closes modal, triggers `logoutUser()` and redirects to `/auth/login`)
+
+#### 5. Error Modal
+
+- **Title:** "Error"
+- **Body:** Display error message from API using `parseErrorResponse(res).msg`.
+- **Button:** "Close"
+
+### Frontend Logic
+
+1. User clicks "Delete My Account" button.
+2. Warning modal opens with email confirmation input.
+3. User types email and clicks "Delete My Account".
+4. Frontend calls `POST /api/user/mark-for-deletion`.
+5. On success:
+ - Close warning modal.
+ - Show success modal.
+ - On "OK" click: call `logoutUser()` from `stores/auth.ts`, redirect to `/auth/login`.
+6. On error:
+ - Close warning modal.
+ - Show error modal with message from API.
+
+---
+
+## Testing
+
+### Backend Tests (`backend/tests/test_user_api.py`)
+
+- [x] Test marking a valid user account (200, `marked_for_deletion = True`, `marked_for_deletion_at` is set).
+- [x] Test marking an already-marked account (return error with `ALREADY_MARKED` or be idempotent).
+- [x] Test marking with invalid JWT (401).
+- [x] Test marking with missing JWT (401).
+- [x] Test login attempt by marked user (403, `ACCOUNT_MARKED_FOR_DELETION`).
+- [x] Test password reset request by marked user (silently ignored, returns 200 but no email sent).
+- [x] Test SSE event is triggered after marking.
+
+### Frontend Tests (`frontend/vue-app/src/components/__tests__/UserProfile.spec.ts`)
+
+- [x] Test "Delete My Account" button renders.
+- [x] Test warning modal opens on button click.
+- [x] Test "Delete My Account" button in modal is disabled until email matches.
+- [x] Test API call is made when user confirms with correct email.
+- [x] Test success modal shows after successful API response.
+- [x] Test error modal shows on API failure (with error message).
+- [x] Test user is signed out after success (calls `logoutUser()`).
+- [x] Test redirect to login page after sign-out.
+- [x] Test button is disabled during loading.
+
+---
+
+## Future Considerations
+
+- A background scheduler will be implemented to physically delete marked accounts after a grace period (e.g., 30 days).
+- Admin panel to view and manage marked accounts.
+- Email notification to user when account is marked for deletion (with grace period details).
+
+---
+
+## Acceptance Criteria (Definition of Done)
+
+### Data Model
+
+- [x] Add `marked_for_deletion` and `marked_for_deletion_at` fields to `User` model (backend).
+- [x] Add matching fields to `User` interface (frontend).
+- [x] Update `to_dict()` and `from_dict()` methods in `User` model.
+
+### Backend
+
+- [x] Create `POST /api/user/mark-for-deletion` endpoint.
+- [x] Add `ACCOUNT_MARKED_FOR_DELETION` and `ALREADY_MARKED` error codes.
+- [x] Block login for marked users in `/api/login`.
+- [x] Block password reset for marked users in `/api/user/request-reset`.
+- [x] Trigger `user_marked_for_deletion` SSE event after marking.
+- [x] All backend tests pass.
+
+### Frontend
+
+- [x] Add "Delete My Account" button to `UserProfile.vue` below "Change Password".
+- [x] Implement warning modal with email confirmation.
+- [x] Implement success modal.
+- [x] Implement error modal.
+- [x] Implement loading state during API call.
+- [x] Sign out user after successful account marking.
+- [x] Redirect to login page after sign-out.
+- [x] Add `ACCOUNT_MARKED_FOR_DELETION` and `ALREADY_MARKED` to `errorCodes.ts`.
+- [x] All frontend tests pass.
diff --git a/.github/specs/active/profile-button-menu/feat-profile-icon-button-menu.md b/.github/specs/archive/profile-button-menu/feat-profile-icon-button-menu.md
similarity index 100%
rename from .github/specs/active/profile-button-menu/feat-profile-icon-button-menu.md
rename to .github/specs/archive/profile-button-menu/feat-profile-icon-button-menu.md
diff --git a/.github/specs/active/profile-button-menu/mockup.png b/.github/specs/archive/profile-button-menu/mockup.png
similarity index 100%
rename from .github/specs/active/profile-button-menu/mockup.png
rename to .github/specs/archive/profile-button-menu/mockup.png
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 978ff98..3bb6c6a 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -37,6 +37,31 @@
"request": "launch",
"url": "https://localhost:5173", // or your Vite dev server port
"webRoot": "${workspaceFolder}/frontend/vue-app"
+ },
+ {
+ "name": "Python: Backend Tests",
+ "type": "python",
+ "request": "launch",
+ "program": "${workspaceFolder}/backend/.venv/Scripts/pytest.exe",
+ "args": [
+ "tests/"
+ ],
+ "cwd": "${workspaceFolder}/backend",
+ "console": "integratedTerminal",
+ "env": {
+ "PYTHONPATH": "${workspaceFolder}/backend"
+ }
+ },
+ {
+ "name": "Vue: Frontend Tests",
+ "type": "node",
+ "request": "launch",
+ "runtimeExecutable": "npx",
+ "runtimeArgs": [
+ "vitest"
+ ],
+ "cwd": "${workspaceFolder}/frontend/vue-app",
+ "console": "integratedTerminal"
}
],
"compounds": [
diff --git a/backend/api/auth_api.py b/backend/api/auth_api.py
index 0597312..42baa11 100644
--- a/backend/api/auth_api.py
+++ b/backend/api/auth_api.py
@@ -12,7 +12,7 @@ from config.paths import get_user_image_dir
from api.error_codes import MISSING_FIELDS, EMAIL_EXISTS, MISSING_TOKEN, INVALID_TOKEN, TOKEN_TIMESTAMP_MISSING, \
TOKEN_EXPIRED, ALREADY_VERIFIED, MISSING_EMAIL, USER_NOT_FOUND, MISSING_EMAIL_OR_PASSWORD, INVALID_CREDENTIALS, \
- NOT_VERIFIED
+ NOT_VERIFIED, ACCOUNT_MARKED_FOR_DELETION
from db.db import users_db
from api.utils import normalize_email
@@ -146,6 +146,10 @@ def login():
if not user.verified:
return jsonify({'error': 'This account has not verified', 'code': NOT_VERIFIED}), 403
+ # Block login for marked accounts
+ if user.marked_for_deletion:
+ return jsonify({'error': 'This account has been marked for deletion and cannot be accessed.', 'code': ACCOUNT_MARKED_FOR_DELETION}), 403
+
payload = {
'email': norm_email,
'user_id': user.id,
@@ -196,12 +200,14 @@ def request_password_reset():
user_dict = users_db.get(UserQuery.email == norm_email)
user = User.from_dict(user_dict) if user_dict else None
if user:
- token = secrets.token_urlsafe(32)
- now_iso = datetime.utcnow().isoformat()
- user.reset_token = token
- user.reset_token_created = now_iso
- users_db.update(user.to_dict(), UserQuery.email == norm_email)
- send_reset_password_email(norm_email, token)
+ # Silently ignore reset requests for marked accounts (don't leak account status)
+ if not user.marked_for_deletion:
+ token = secrets.token_urlsafe(32)
+ now_iso = datetime.utcnow().isoformat()
+ user.reset_token = token
+ user.reset_token_created = now_iso
+ users_db.update(user.to_dict(), UserQuery.email == norm_email)
+ send_reset_password_email(norm_email, token)
return jsonify({'message': success_msg}), 200
diff --git a/backend/api/error_codes.py b/backend/api/error_codes.py
index 2ec88f0..39d567f 100644
--- a/backend/api/error_codes.py
+++ b/backend/api/error_codes.py
@@ -9,4 +9,6 @@ USER_NOT_FOUND = "USER_NOT_FOUND"
ALREADY_VERIFIED = "ALREADY_VERIFIED"
MISSING_EMAIL_OR_PASSWORD = "MISSING_EMAIL_OR_PASSWORD"
INVALID_CREDENTIALS = "INVALID_CREDENTIALS"
-NOT_VERIFIED = "NOT_VERIFIED"
\ No newline at end of file
+NOT_VERIFIED = "NOT_VERIFIED"
+ACCOUNT_MARKED_FOR_DELETION = "ACCOUNT_MARKED_FOR_DELETION"
+ALREADY_MARKED = "ALREADY_MARKED"
\ No newline at end of file
diff --git a/backend/api/user_api.py b/backend/api/user_api.py
index d7d58d4..7ef3491 100644
--- a/backend/api/user_api.py
+++ b/backend/api/user_api.py
@@ -1,4 +1,5 @@
from flask import Blueprint, request, jsonify, current_app
+from events.types.user_modified import UserModified
from models.user import User
from tinydb import Query
from db.db import users_db
@@ -6,8 +7,11 @@ import jwt
import random
import string
import utils.email_sender as email_sender
-from datetime import datetime, timedelta
-from api.utils import get_validated_user_id
+from datetime import datetime, timedelta, timezone
+from api.utils import get_validated_user_id, normalize_email, send_event_for_current_user
+from api.error_codes import ACCOUNT_MARKED_FOR_DELETION, ALREADY_MARKED
+from events.types.event_types import EventType
+from events.types.event import Event
user_api = Blueprint('user_api', __name__)
UserQuery = Query()
@@ -170,3 +174,36 @@ def set_pin():
user.pin_setup_code_created = None
users_db.update(user.to_dict(), UserQuery.email == user.email)
return jsonify({'message': 'Parent PIN set'}), 200
+
+@user_api.route('/user/mark-for-deletion', methods=['POST'])
+def mark_for_deletion():
+ user_id = get_validated_user_id()
+ if not user_id:
+ return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
+ user = get_current_user()
+ if not user:
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ # Validate email from request body
+ data = request.get_json()
+ email = data.get('email', '').strip()
+ if not email:
+ return jsonify({'error': 'Email is required', 'code': 'EMAIL_REQUIRED'}), 400
+
+ # Verify email matches the logged-in user - make sure to normalize the email address first
+ if normalize_email(email) != normalize_email(user.email):
+ return jsonify({'error': 'Email does not match your account', 'code': 'EMAIL_MISMATCH'}), 400
+
+ # Check if already marked
+ if user.marked_for_deletion:
+ return jsonify({'error': 'Account already marked for deletion', 'code': ALREADY_MARKED}), 400
+
+ # Mark for deletion
+ user.marked_for_deletion = True
+ user.marked_for_deletion_at = datetime.now(timezone.utc).isoformat()
+ users_db.update(user.to_dict(), UserQuery.id == user.id)
+
+ # Trigger SSE event
+ send_event_for_current_user(Event(EventType.USER_MARKED_FOR_DELETION.value, UserModified(user.id, UserModified.OPERATION_DELETE)))
+
+ return jsonify({'success': True}), 200
diff --git a/backend/events/types/event_types.py b/backend/events/types/event_types.py
index 0b9a08b..797e74c 100644
--- a/backend/events/types/event_types.py
+++ b/backend/events/types/event_types.py
@@ -13,3 +13,5 @@ class EventType(Enum):
CHILD_REWARD_REQUEST = "child_reward_request"
CHILD_MODIFIED = "child_modified"
+
+ USER_MARKED_FOR_DELETION = "user_marked_for_deletion"
diff --git a/backend/events/types/user_modified.py b/backend/events/types/user_modified.py
new file mode 100644
index 0000000..8a57782
--- /dev/null
+++ b/backend/events/types/user_modified.py
@@ -0,0 +1,21 @@
+from events.types.payload import Payload
+
+
+class UserModified(Payload):
+ OPERATION_ADD = "ADD"
+ OPERATION_EDIT = "EDIT"
+ OPERATION_DELETE = "DELETE"
+ def __init__(self, user_id: str, operation: str):
+ super().__init__({
+ 'user_id': user_id,
+ 'operation': operation,
+ })
+
+ @property
+ def user_id(self) -> str:
+ return self.get("user_id")
+
+ @property
+ def operation(self) -> str:
+ return self.get("operation")
+
diff --git a/backend/models/user.py b/backend/models/user.py
index cb86f61..b4bc84c 100644
--- a/backend/models/user.py
+++ b/backend/models/user.py
@@ -16,6 +16,8 @@ class User(BaseModel):
pin: str = ''
pin_setup_code: str = ''
pin_setup_code_created: str | None = None
+ marked_for_deletion: bool = False
+ marked_for_deletion_at: str | None = None
@classmethod
def from_dict(cls, d: dict):
@@ -33,6 +35,8 @@ class User(BaseModel):
pin=d.get('pin', ''),
pin_setup_code=d.get('pin_setup_code', ''),
pin_setup_code_created=d.get('pin_setup_code_created'),
+ marked_for_deletion=d.get('marked_for_deletion', False),
+ marked_for_deletion_at=d.get('marked_for_deletion_at'),
id=d.get('id'),
created_at=d.get('created_at'),
updated_at=d.get('updated_at')
@@ -54,6 +58,8 @@ class User(BaseModel):
'image_id': self.image_id,
'pin': self.pin,
'pin_setup_code': self.pin_setup_code,
- 'pin_setup_code_created': self.pin_setup_code_created
+ 'pin_setup_code_created': self.pin_setup_code_created,
+ 'marked_for_deletion': self.marked_for_deletion,
+ 'marked_for_deletion_at': self.marked_for_deletion_at
})
return base
diff --git a/backend/tests/test_user_api.py b/backend/tests/test_user_api.py
new file mode 100644
index 0000000..056933f
--- /dev/null
+++ b/backend/tests/test_user_api.py
@@ -0,0 +1,177 @@
+import pytest
+from datetime import datetime, timezone
+from flask import Flask
+from api.user_api import user_api
+from api.auth_api import auth_api
+from db.db import users_db
+from tinydb import Query
+import jwt
+
+# Test user credentials
+TEST_EMAIL = "usertest@example.com"
+TEST_PASSWORD = "testpass123"
+MARKED_EMAIL = "marked@example.com"
+MARKED_PASSWORD = "markedpass"
+
+def add_test_users():
+ """Add test users to the database."""
+ # Remove if exists
+ users_db.remove(Query().email == TEST_EMAIL)
+ users_db.remove(Query().email == MARKED_EMAIL)
+
+ # Add regular test user
+ users_db.insert({
+ "id": "test_user_id",
+ "first_name": "Test",
+ "last_name": "User",
+ "email": TEST_EMAIL,
+ "password": TEST_PASSWORD,
+ "verified": True,
+ "image_id": "boy01",
+ "marked_for_deletion": False,
+ "marked_for_deletion_at": None
+ })
+
+ # Add user already marked for deletion
+ users_db.insert({
+ "id": "marked_user_id",
+ "first_name": "Marked",
+ "last_name": "User",
+ "email": MARKED_EMAIL,
+ "password": MARKED_PASSWORD,
+ "verified": True,
+ "image_id": "girl01",
+ "marked_for_deletion": True,
+ "marked_for_deletion_at": "2024-01-15T10:30:00+00:00"
+ })
+
+def login_and_get_token(client, email, password):
+ """Login and extract JWT token from response."""
+ resp = client.post('/login', json={"email": email, "password": password})
+ assert resp.status_code == 200
+ # Extract token from Set-Cookie header
+ set_cookie = resp.headers.get("Set-Cookie")
+ assert set_cookie and "token=" in set_cookie
+ # Flask test client automatically handles cookies
+ return resp
+
+@pytest.fixture
+def client():
+ """Setup Flask test client with registered blueprints."""
+ app = Flask(__name__)
+ app.register_blueprint(user_api)
+ app.register_blueprint(auth_api)
+ app.config['TESTING'] = True
+ app.config['SECRET_KEY'] = 'supersecretkey'
+ app.config['FRONTEND_URL'] = 'http://localhost:5173' # Needed for email_sender
+ with app.test_client() as client:
+ add_test_users()
+ yield client
+
+@pytest.fixture
+def authenticated_client(client):
+ """Setup client with authenticated user session."""
+ login_and_get_token(client, TEST_EMAIL, TEST_PASSWORD)
+ return client
+
+@pytest.fixture
+def marked_client(client):
+ """Setup client with marked-for-deletion user session."""
+ login_and_get_token(client, MARKED_EMAIL, MARKED_PASSWORD)
+ return client
+
+def test_mark_user_for_deletion_success(authenticated_client):
+ """Test successfully marking a user account for deletion."""
+ response = authenticated_client.post('/user/mark-for-deletion', json={"email": TEST_EMAIL})
+ assert response.status_code == 200
+ data = response.get_json()
+ assert data['success'] is True
+
+ # Verify database was updated
+ UserQuery = Query()
+ user = users_db.search(UserQuery.email == TEST_EMAIL)[0]
+ assert user['marked_for_deletion'] is True
+ assert user['marked_for_deletion_at'] is not None
+
+ # Verify timestamp is valid ISO format
+ marked_at = datetime.fromisoformat(user['marked_for_deletion_at'])
+ assert marked_at.tzinfo is not None
+
+def test_login_for_marked_user_returns_403(client):
+ """Test that login for a marked-for-deletion user returns 403 Forbidden."""
+ response = client.post('/login', json={
+ "email": MARKED_EMAIL,
+ "password": MARKED_PASSWORD
+ })
+ assert response.status_code == 403
+ data = response.get_json()
+ assert 'error' in data
+ assert data['code'] == 'ACCOUNT_MARKED_FOR_DELETION'
+
+def test_mark_for_deletion_requires_auth(client):
+ """Test that marking for deletion requires authentication."""
+ response = client.post('/user/mark-for-deletion', json={"email": TEST_EMAIL})
+ assert response.status_code == 401
+ data = response.get_json()
+ assert 'error' in data
+
+def test_login_blocked_for_marked_user(client):
+ """Test that login is blocked for users marked for deletion."""
+ response = client.post('/login', json={
+ "email": MARKED_EMAIL,
+ "password": MARKED_PASSWORD
+ })
+ assert response.status_code == 403
+ data = response.get_json()
+ assert 'error' in data
+ assert data['code'] == 'ACCOUNT_MARKED_FOR_DELETION'
+
+def test_login_succeeds_for_unmarked_user(client):
+ """Test that login works normally for users not marked for deletion."""
+ response = client.post('/login', json={
+ "email": TEST_EMAIL,
+ "password": TEST_PASSWORD
+ })
+ assert response.status_code == 200
+ data = response.get_json()
+ assert 'message' in data
+
+def test_password_reset_ignored_for_marked_user(client):
+ """Test that password reset requests are silently ignored for marked users."""
+ response = client.post('/request-password-reset', json={"email": MARKED_EMAIL})
+ assert response.status_code == 200
+ data = response.get_json()
+ assert 'message' in data
+
+def test_password_reset_works_for_unmarked_user(client):
+ """Test that password reset works normally for unmarked users."""
+ response = client.post('/request-password-reset', json={"email": TEST_EMAIL})
+ assert response.status_code == 200
+ data = response.get_json()
+ assert 'message' in data
+
+def test_mark_for_deletion_updates_timestamp(authenticated_client):
+ """Test that marking for deletion sets a proper timestamp."""
+ before_time = datetime.now(timezone.utc)
+
+ response = authenticated_client.post('/user/mark-for-deletion', json={"email": TEST_EMAIL})
+ assert response.status_code == 200
+
+ after_time = datetime.now(timezone.utc)
+
+ # Verify timestamp is between before and after
+ UserQuery = Query()
+ user = users_db.search(UserQuery.email == TEST_EMAIL)[0]
+ marked_at = datetime.fromisoformat(user['marked_for_deletion_at'])
+
+ assert before_time <= marked_at <= after_time
+
+def test_mark_for_deletion_with_invalid_jwt(client):
+ """Test marking for deletion with invalid JWT token."""
+ # Set invalid cookie manually
+ client.set_cookie('token', 'invalid.jwt.token')
+
+ response = client.post('/user/mark-for-deletion', json={})
+ assert response.status_code == 401
+ data = response.get_json()
+ assert 'error' in data
diff --git a/frontend/vue-app/src/__tests__/UserProfile.spec.ts b/frontend/vue-app/src/__tests__/UserProfile.spec.ts
new file mode 100644
index 0000000..dc7c8ce
--- /dev/null
+++ b/frontend/vue-app/src/__tests__/UserProfile.spec.ts
@@ -0,0 +1,285 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { mount, VueWrapper, flushPromises } from '@vue/test-utils'
+import { nextTick } from 'vue'
+import UserProfile from '../components/profile/UserProfile.vue'
+import { createMemoryHistory, createRouter } from 'vue-router'
+
+// Mock fetch globally
+global.fetch = vi.fn()
+
+// Mock router
+const mockRouter = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ { path: '/auth/login', name: 'Login' },
+ { path: '/profile', name: 'UserProfile' },
+ ],
+})
+
+// Mock auth store
+const mockLogoutUser = vi.fn()
+vi.mock('../stores/auth', () => ({
+ logoutUser: () => mockLogoutUser(),
+}))
+
+describe('UserProfile - Delete Account', () => {
+ let wrapper: VueWrapper