fix(server): harden self-hosted server — pgvector upgrade, admin auth, endpoint security (#5360)
This commit is contained in:
+2
-1
@@ -302,7 +302,8 @@
|
||||
"group": "Migration",
|
||||
"icon": "arrow-right",
|
||||
"pages": [
|
||||
"migration/oss-v2-to-v3"
|
||||
"migration/oss-v2-to-v3",
|
||||
"migration/server-pgvector-upgrade"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -228,6 +228,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
|
||||
- [OSS v2 to v3 Migration](https://docs.mem0.ai/migration/oss-v2-to-v3) [OSS]: Use when upgrading a self-hosted deployment across major versions.
|
||||
- [Platform v2 to v3 Migration](https://docs.mem0.ai/migration/platform-v2-to-v3) [Platform]: Use when upgrading a Platform integration across major versions.
|
||||
- [API Changes](https://docs.mem0.ai/migration/api-changes) [Both]: Use when the upgrade involves API surface changes.
|
||||
- [Server pgvector Image Upgrade](https://docs.mem0.ai/migration/server-pgvector-upgrade) [OSS]: Use when upgrading the self-hosted server Docker image from ankane/pgvector to pgvector/pgvector.
|
||||
- [Changelog](https://docs.mem0.ai/changelog/highlights) [Both]: Use when the user asks what shipped recently.
|
||||
|
||||
## Open Source
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: "Server: Upgrading the pgvector Docker Image"
|
||||
description: "Migrate your self-hosted Mem0 server from the archived ankane/pgvector image to the official pgvector/pgvector image."
|
||||
icon: "arrow-right"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The self-hosted Mem0 server has upgraded its PostgreSQL Docker image:
|
||||
|
||||
| | Before | After |
|
||||
| --- | --- | --- |
|
||||
| Docker image | `ankane/pgvector:v0.5.1` | `pgvector/pgvector:pg17` |
|
||||
| PostgreSQL | 15 | 17 |
|
||||
| pgvector | 0.5.1 | 0.8.0 |
|
||||
| Credentials | Hardcoded `postgres` / `postgres` | Set via `POSTGRES_USER` / `POSTGRES_PASSWORD` env vars |
|
||||
|
||||
<Warning>
|
||||
The `ankane/pgvector` image is **archived and no longer maintained**. The new `pgvector/pgvector` image is the official distribution maintained by the pgvector project.
|
||||
</Warning>
|
||||
|
||||
<Info>
|
||||
**Should you migrate?**
|
||||
- You are running the Mem0 server via `docker-compose.yaml` in the `server/` directory.
|
||||
- You want to stay on a maintained, actively-patched PostgreSQL + pgvector image.
|
||||
- You want pgvector 0.8.0 features (improved HNSW performance, parallel index builds).
|
||||
</Info>
|
||||
|
||||
## Fresh Installs
|
||||
|
||||
No migration is needed. Copy the example env file, set your password, and start the stack:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
cp .env.example .env
|
||||
# Edit .env — set POSTGRES_PASSWORD (required) and OPENAI_API_KEY at minimum
|
||||
make up
|
||||
```
|
||||
|
||||
## Migrating an Existing Install
|
||||
|
||||
PostgreSQL 17 cannot read data files created by PostgreSQL 15 directly. You need to export your data from the old container and import it into the new one.
|
||||
|
||||
### 1. Back Up Your Data
|
||||
|
||||
With the **old** stack still running:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
docker compose exec -T postgres pg_dumpall -U postgres > mem0_backup.sql
|
||||
```
|
||||
|
||||
Verify the dump is non-empty:
|
||||
|
||||
```bash
|
||||
ls -lh mem0_backup.sql
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Do not skip this step. The next step permanently deletes your Postgres data volume.
|
||||
</Warning>
|
||||
|
||||
### 2. Stop the Old Stack and Remove the Volume
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
### 3. Update Your `.env`
|
||||
|
||||
Postgres credentials are no longer hardcoded in `docker-compose.yaml`. Add them to your `.env`:
|
||||
|
||||
```bash
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=<your-password> # required — compose will refuse to start without it
|
||||
POSTGRES_COLLECTION_NAME=memories
|
||||
```
|
||||
|
||||
<Info>
|
||||
`POSTGRES_PASSWORD` is **required** — `docker compose up` will refuse to start without it. If you previously relied on the hardcoded default, set `POSTGRES_PASSWORD=postgres`.
|
||||
</Info>
|
||||
|
||||
### 4. Start Only Postgres
|
||||
|
||||
Start **only** the Postgres container first — do **not** start the mem0 API yet.
|
||||
The API runs `alembic upgrade head` on startup, which creates empty tables that
|
||||
would conflict with the restore.
|
||||
|
||||
```bash
|
||||
docker compose up -d postgres
|
||||
```
|
||||
|
||||
Wait for Postgres to become healthy:
|
||||
|
||||
```bash
|
||||
docker compose exec -T postgres pg_isready -q && echo "ready" || echo "not ready"
|
||||
```
|
||||
|
||||
### 5. Restore Your Data
|
||||
|
||||
```bash
|
||||
docker compose exec -T postgres psql -U postgres < mem0_backup.sql
|
||||
```
|
||||
|
||||
You may see notices like `role "postgres" already exists` — these are safe to ignore.
|
||||
|
||||
<Warning>
|
||||
You must restore **before** starting the mem0 API container. The API runs
|
||||
database migrations on startup which create empty tables — restoring after
|
||||
that would fail with duplicate-key errors and lose your API keys and settings.
|
||||
</Warning>
|
||||
|
||||
### 6. Start the API
|
||||
|
||||
Now start the mem0 API container. Alembic will detect the existing tables and
|
||||
only apply any new migrations:
|
||||
|
||||
```bash
|
||||
docker compose up -d mem0
|
||||
```
|
||||
|
||||
### 7. Verify
|
||||
|
||||
```bash
|
||||
# Check service health
|
||||
cd server && make health
|
||||
|
||||
# Confirm memories are accessible
|
||||
curl -s http://localhost:8888/memories?user_id=<your-user-id> \
|
||||
-H "X-API-Key: <your-api-key>"
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
If something goes wrong, revert the image tag in `docker-compose.yaml`:
|
||||
|
||||
```yaml
|
||||
postgres:
|
||||
image: ankane/pgvector:v0.5.1
|
||||
```
|
||||
|
||||
Then destroy the new volume, start the old image, and restore from your backup:
|
||||
|
||||
```bash
|
||||
docker compose down -v
|
||||
docker compose up -d --build
|
||||
docker compose exec -T postgres psql -U postgres < mem0_backup.sql
|
||||
```
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Join our [Discord community](https://mem0.ai/discord) for real-time support
|
||||
- Open an issue on [GitHub](https://github.com/mem0ai/mem0/issues)
|
||||
+6
-5
@@ -4,12 +4,13 @@ OPENAI_API_KEY=
|
||||
# ANTHROPIC_API_KEY=
|
||||
# GOOGLE_API_KEY=
|
||||
|
||||
POSTGRES_HOST=
|
||||
POSTGRES_PORT=
|
||||
POSTGRES_DB=
|
||||
POSTGRES_USER=
|
||||
# Postgres — POSTGRES_PASSWORD is required; docker-compose will not start without it.
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=
|
||||
POSTGRES_COLLECTION_NAME=
|
||||
POSTGRES_COLLECTION_NAME=memories
|
||||
|
||||
ADMIN_API_KEY=
|
||||
JWT_SECRET=
|
||||
|
||||
@@ -2,8 +2,24 @@
|
||||
|
||||
Mem0 ships a self-hosted FastAPI server plus a local dashboard. It is secure by default, supports dashboard login and API keys, and exposes OpenAPI docs at `/docs`.
|
||||
|
||||
> **Upgrading?** The Postgres image changed from the archived `ankane/pgvector:v0.5.1`
|
||||
> to the official `pgvector/pgvector:pg17`, and `POSTGRES_PASSWORD` is now a required
|
||||
> env var. If you have an existing install, see
|
||||
> [Migrating from ankane/pgvector to pgvector/pgvector](#migrating-from-ankanepgvector-to-pgvectorpgvector)
|
||||
> before running `docker compose up`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Copy the example env file and set a Postgres password (required):
|
||||
|
||||
```bash
|
||||
cd server
|
||||
cp .env.example .env
|
||||
# Edit .env — at minimum set POSTGRES_PASSWORD and OPENAI_API_KEY
|
||||
```
|
||||
|
||||
### Agent-first
|
||||
|
||||
Run one command; the terminal prints the admin email, password, and first API key.
|
||||
@@ -121,6 +137,140 @@ The dashboard sets the following response headers on every path (see `server/das
|
||||
|
||||
Together these prevent iframe embedding, sniffing of mislabelled MIME types, and cross-origin referrer leaks. Harden further behind your own reverse proxy if needed.
|
||||
|
||||
## Migrating from `ankane/pgvector` to `pgvector/pgvector`
|
||||
|
||||
The `ankane/pgvector` Docker image is archived and no longer maintained. This release
|
||||
replaces it with the official `pgvector/pgvector:pg17` image (PostgreSQL 17, pgvector 0.8.0).
|
||||
|
||||
**What changed:**
|
||||
|
||||
| | Before | After |
|
||||
|---|---|---|
|
||||
| Docker image | `ankane/pgvector:v0.5.1` | `pgvector/pgvector:pg17` |
|
||||
| PostgreSQL version | 15 | 17 |
|
||||
| pgvector version | 0.5.1 | 0.8.0 |
|
||||
| Credentials | Hardcoded `postgres`/`postgres` | Driven by `POSTGRES_USER` / `POSTGRES_PASSWORD` env vars |
|
||||
|
||||
### Fresh installs (no existing data)
|
||||
|
||||
No migration needed. Copy `.env.example` to `.env`, set `POSTGRES_PASSWORD`, and run:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
make up
|
||||
```
|
||||
|
||||
### Existing installs (preserving data)
|
||||
|
||||
PostgreSQL 17 cannot read data files written by PostgreSQL 15 directly.
|
||||
You must export your data first, then import it into the new container.
|
||||
|
||||
**1. Export your data from the old container**
|
||||
|
||||
With the old stack still running:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
|
||||
# Dump all databases (mem0 memories + mem0_app auth/config data)
|
||||
docker compose exec -T postgres pg_dumpall -U postgres > mem0_backup.sql
|
||||
```
|
||||
|
||||
Verify the dump file is non-empty:
|
||||
|
||||
```bash
|
||||
ls -lh mem0_backup.sql
|
||||
```
|
||||
|
||||
**2. Stop the old stack and remove the old volume**
|
||||
|
||||
```bash
|
||||
# Stop containers
|
||||
docker compose down
|
||||
|
||||
# Remove the old Postgres data volume
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
> **Warning:** `docker compose down -v` deletes the `postgres_db` volume permanently.
|
||||
> Only run this after you have verified your backup.
|
||||
|
||||
**3. Update your `.env`**
|
||||
|
||||
The Postgres credentials are no longer hardcoded in `docker-compose.yaml`.
|
||||
Add them to your `.env` file (or verify they match your old setup):
|
||||
|
||||
```bash
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=<your-password> # required — compose will refuse to start without it
|
||||
POSTGRES_COLLECTION_NAME=memories
|
||||
```
|
||||
|
||||
If you previously relied on the hardcoded defaults (`postgres`/`postgres`), set
|
||||
`POSTGRES_PASSWORD=postgres` to keep the same credentials.
|
||||
|
||||
**4. Start only Postgres**
|
||||
|
||||
Start **only** the Postgres container first — do not start the mem0 API yet.
|
||||
The API runs `alembic upgrade head` on startup, which creates empty tables that
|
||||
would conflict with the restore.
|
||||
|
||||
```bash
|
||||
docker compose up -d postgres
|
||||
```
|
||||
|
||||
Wait for the Postgres healthcheck to pass:
|
||||
|
||||
```bash
|
||||
docker compose exec -T postgres pg_isready -q && echo "ready" || echo "not ready"
|
||||
```
|
||||
|
||||
**5. Restore your data**
|
||||
|
||||
```bash
|
||||
docker compose exec -T postgres psql -U postgres < mem0_backup.sql
|
||||
```
|
||||
|
||||
You may see notices like `role "postgres" already exists` — these are harmless.
|
||||
|
||||
> **Important:** You must restore before starting the mem0 API container. The API
|
||||
> runs database migrations on startup which create empty tables — restoring after
|
||||
> that would fail with duplicate-key errors and lose your API keys and settings.
|
||||
|
||||
**6. Start the API**
|
||||
|
||||
Now start the mem0 API container. Alembic will detect the existing tables and
|
||||
only apply any new migrations:
|
||||
|
||||
```bash
|
||||
docker compose up -d mem0
|
||||
```
|
||||
|
||||
**7. Verify**
|
||||
|
||||
```bash
|
||||
# Check the API is healthy
|
||||
make health
|
||||
|
||||
# Confirm your memories are present
|
||||
curl -s http://localhost:8888/memories?user_id=<your-user-id> -H "X-API-Key: <your-api-key>"
|
||||
```
|
||||
|
||||
### Rollback
|
||||
|
||||
If you need to revert, restore the old image tag in `docker-compose.yaml`:
|
||||
|
||||
```yaml
|
||||
postgres:
|
||||
image: ankane/pgvector:v0.5.1
|
||||
```
|
||||
|
||||
Then `docker compose down -v`, `docker compose up -d --build`, and restore from
|
||||
`mem0_backup.sql` into the old container the same way.
|
||||
|
||||
## Reference
|
||||
|
||||
Additional product and API documentation lives at [docs.mem0.ai](https://docs.mem0.ai/open-source/overview).
|
||||
|
||||
+32
-3
@@ -3,16 +3,15 @@ import secrets
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from db import get_db
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer
|
||||
from jose import JWTError, jwt
|
||||
from models import APIKey, RefreshTokenJti, User
|
||||
from passlib.context import CryptContext
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from db import get_db
|
||||
from models import APIKey, RefreshTokenJti, User
|
||||
|
||||
JWT_SECRET = os.environ.get("JWT_SECRET", "")
|
||||
JWT_ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
@@ -184,3 +183,33 @@ async def require_auth(
|
||||
return default_user
|
||||
raise HTTPException(status_code=401, detail="Authentication required.")
|
||||
return user
|
||||
|
||||
|
||||
_BOOTSTRAP_ADMIN = User(
|
||||
id=uuid.UUID(int=0), name="admin_api_key", email="", password_hash="", role="admin", created_at=datetime.min.replace(tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
async def require_admin(
|
||||
request: Request,
|
||||
user: User | None = Depends(verify_auth),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""Like require_auth but also enforces admin role.
|
||||
|
||||
ADMIN_API_KEY and AUTH_DISABLED callers are treated as admin even when
|
||||
the users table is empty (fresh-deploy bootstrap).
|
||||
"""
|
||||
auth_type = getattr(request.state, "auth_type", "none")
|
||||
if user is None:
|
||||
if auth_type in {"admin_api_key", "disabled"}:
|
||||
default_user = _get_default_user(db)
|
||||
if default_user is not None:
|
||||
if default_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="Admin role required.")
|
||||
return default_user
|
||||
return _BOOTSTRAP_ADMIN
|
||||
raise HTTPException(status_code=401, detail="Authentication required.")
|
||||
if user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="Admin role required.")
|
||||
return user
|
||||
|
||||
@@ -30,16 +30,16 @@ services:
|
||||
- MEM0_TELEMETRY=${MEM0_TELEMETRY:-true}
|
||||
|
||||
postgres:
|
||||
image: ankane/pgvector:v0.5.1
|
||||
image: pgvector/pgvector:pg17
|
||||
restart: on-failure
|
||||
shm_size: "128mb"
|
||||
networks:
|
||||
- mem0_network
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-q", "-d", "postgres", "-U", "postgres"]
|
||||
test: ["CMD-SHELL", "pg_isready -q -d postgres -U ${POSTGRES_USER:-postgres}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
+14
-8
@@ -5,7 +5,7 @@ import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import telemetry
|
||||
from auth import ADMIN_API_KEY, AUTH_DISABLED, JWT_SECRET, verify_auth
|
||||
from auth import ADMIN_API_KEY, AUTH_DISABLED, JWT_SECRET, require_admin, verify_auth
|
||||
from db import SessionLocal
|
||||
from dotenv import load_dotenv
|
||||
from errors import (
|
||||
@@ -316,8 +316,8 @@ def list_bundled_providers(_auth=Depends(verify_auth)):
|
||||
|
||||
|
||||
@app.post("/configure", summary="Configure Mem0")
|
||||
def set_config(config: Dict[str, Any], _auth=Depends(verify_auth)):
|
||||
"""Set memory configuration."""
|
||||
def set_config(config: Dict[str, Any], _auth=Depends(require_admin)):
|
||||
"""Set memory configuration. Requires admin role."""
|
||||
_validate_bundled_providers(config)
|
||||
update_config(config)
|
||||
return {"message": "Configuration set successfully"}
|
||||
@@ -391,19 +391,25 @@ def _list_all_memories(limit: int = ALL_MEMORIES_LIMIT) -> Dict[str, Any]:
|
||||
|
||||
@app.get("/memories", summary="Get memories")
|
||||
def get_all_memories(
|
||||
request: Request,
|
||||
user_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
_auth=Depends(verify_auth),
|
||||
):
|
||||
"""Retrieve stored memories. Lists all memories when no identifier is provided."""
|
||||
"""Retrieve stored memories. Lists all memories when no identifier is provided (admin only)."""
|
||||
try:
|
||||
if not any([user_id, run_id, agent_id]):
|
||||
auth_type = getattr(request.state, "auth_type", "none")
|
||||
if _auth is not None and _auth.role != "admin" and auth_type not in {"admin_api_key", "disabled"}:
|
||||
raise HTTPException(status_code=403, detail="Admin role required to list all memories.")
|
||||
return _list_all_memories()
|
||||
filters = {
|
||||
k: v for k, v in {"user_id": user_id, "run_id": run_id, "agent_id": agent_id}.items() if v is not None
|
||||
}
|
||||
return get_memory_instance().get_all(filters=filters)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
raise upstream_error()
|
||||
|
||||
@@ -479,9 +485,9 @@ def delete_all_memories(
|
||||
user_id: Optional[str] = None,
|
||||
run_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
_auth=Depends(verify_auth),
|
||||
_auth=Depends(require_admin),
|
||||
):
|
||||
"""Delete all memories for a given identifier."""
|
||||
"""Delete all memories for a given identifier. Requires admin role."""
|
||||
if not any([user_id, run_id, agent_id]):
|
||||
raise HTTPException(status_code=400, detail="At least one identifier is required.")
|
||||
try:
|
||||
@@ -495,8 +501,8 @@ def delete_all_memories(
|
||||
|
||||
|
||||
@app.post("/reset", summary="Reset all memories")
|
||||
def reset_memory(_auth=Depends(verify_auth)):
|
||||
"""Completely reset stored memories."""
|
||||
def reset_memory(_auth=Depends(require_admin)):
|
||||
"""Completely reset stored memories. Requires admin role."""
|
||||
try:
|
||||
get_memory_instance().reset()
|
||||
return {"message": "All memories reset"}
|
||||
|
||||
@@ -2,11 +2,10 @@ from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal, Optional
|
||||
|
||||
from auth import require_admin, verify_auth
|
||||
from errors import upstream_error
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
|
||||
from auth import verify_auth
|
||||
from errors import upstream_error
|
||||
from schemas import MessageResponse
|
||||
from server_state import get_memory_instance
|
||||
|
||||
@@ -69,7 +68,7 @@ def list_entities(_auth=Depends(verify_auth)):
|
||||
|
||||
|
||||
@router.delete("/{entity_type}/{entity_id}", response_model=MessageResponse)
|
||||
def delete_entity(entity_type: EntityType, entity_id: str, _auth=Depends(verify_auth)):
|
||||
def delete_entity(entity_type: EntityType, entity_id: str, _auth=Depends(require_admin)):
|
||||
try:
|
||||
get_memory_instance().delete_all(**{TYPE_TO_FIELD[entity_type]: entity_id})
|
||||
except Exception:
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from auth import require_admin
|
||||
from db import get_db
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from models import RequestLog
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from auth import require_auth
|
||||
from db import get_db
|
||||
from models import RequestLog, User
|
||||
|
||||
router = APIRouter(prefix="/requests", tags=["requests"])
|
||||
|
||||
|
||||
@@ -30,7 +29,7 @@ API_KEY_AUTH_TYPES = ("api_key", "admin_api_key")
|
||||
|
||||
@router.get("", response_model=list[RequestLogItem])
|
||||
def list_requests(
|
||||
user: User = Depends(require_auth),
|
||||
_auth=Depends(require_admin),
|
||||
db: Session = Depends(get_db),
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user