Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation
This commit is contained in:
389
LinkSyncServer/design.md
Normal file
389
LinkSyncServer/design.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# LinkSyncServer - Design Documentation
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
LinkSyncServer is a Python/FastAPI web application with PostgreSQL as the database, designed for bookmark management with advanced collection and query capabilities.
|
||||
|
||||
### Tech Stack
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|------------|
|
||||
| Framework | FastAPI |
|
||||
| ORM | SQLAlchemy |
|
||||
| Authentication | JWT (PyJWT) |
|
||||
| Database | PostgreSQL 15+ |
|
||||
| Templates | Jinja2 |
|
||||
| Static Files | Native static serving |
|
||||
| Containerization | Docker |
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
LinkSyncServer/
|
||||
├── README.md
|
||||
├── TODOs.txt
|
||||
├── design.md
|
||||
├── tasks.md
|
||||
├── AGENTS.md
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── app.py
|
||||
├── config/
|
||||
│ ├── settings.py
|
||||
│ └── schema.sql
|
||||
├── api/
|
||||
│ ├── __init__.py
|
||||
│ ├── routes.py
|
||||
│ ├── endpoints/
|
||||
│ │ ├── auth.py
|
||||
│ │ ├── links.py
|
||||
│ │ ├── collections.py
|
||||
│ │ └── queries.py
|
||||
│ ├── parsers/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── query_parser.py
|
||||
│ └── serializers/
|
||||
│ ├── __init__.py
|
||||
│ └── schemas.py
|
||||
├── models/
|
||||
│ ├── __init__.py
|
||||
│ ├── base.py
|
||||
│ ├── user.py
|
||||
│ ├── link.py
|
||||
│ ├── collection.py
|
||||
│ └── tag.py
|
||||
├── queries/
|
||||
│ ├── __init__.py
|
||||
│ ├── ast.py
|
||||
│ └── executor.py
|
||||
├── templates/
|
||||
│ ├── base.html
|
||||
│ ├── layout.html
|
||||
│ ├── links/
|
||||
│ │ ├── list.html
|
||||
│ │ ├── detail.html
|
||||
│ │ └── create.html
|
||||
│ ├── collections/
|
||||
│ │ ├── list.html
|
||||
│ │ ├── detail.html
|
||||
│ │ ├── create.html
|
||||
│ │ └── edit.html
|
||||
│ └── auth/
|
||||
│ ├── login.html
|
||||
│ ├── register.html
|
||||
│ └── forgot_password.html
|
||||
├── static/
|
||||
│ ├── css/
|
||||
│ │ ├── main.css
|
||||
│ │ └── print.css
|
||||
│ ├── js/
|
||||
│ │ ├── main.js
|
||||
│ │ └── api.js
|
||||
│ └── images/
|
||||
└── tests/
|
||||
├── __init__.py
|
||||
├── conftest.py
|
||||
├── test_auth.py
|
||||
├── test_links.py
|
||||
└── test_collections.py
|
||||
```
|
||||
|
||||
## Data Models
|
||||
|
||||
### User
|
||||
|
||||
```python
|
||||
class User(Base):
|
||||
id: UUID
|
||||
username: str (unique, indexed)
|
||||
email: str (unique, indexed)
|
||||
password_hash: str
|
||||
role: Enum('admin', 'user')
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
```
|
||||
|
||||
### Link
|
||||
|
||||
```python
|
||||
class Link(Base):
|
||||
id: UUID
|
||||
url: str (indexed)
|
||||
title: str
|
||||
description: str | None
|
||||
notes: str | None
|
||||
tags: List[UUID] (FK to tags)
|
||||
favicon_url: str | None
|
||||
path: str (folder structure)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
visit_count: int
|
||||
is_bookmarked: bool
|
||||
source_set_id: UUID | None (FK to collections)
|
||||
user_id: UUID (FK, nullable for shared links)
|
||||
```
|
||||
|
||||
### Collection
|
||||
|
||||
```python
|
||||
class Collection(Base):
|
||||
id: UUID
|
||||
name: str (unique per user)
|
||||
description: str | None
|
||||
query_type: Enum('static', 'dynamic')
|
||||
query_expression: str | None # SQL-like query string
|
||||
links: List[UUID] # For static collections only
|
||||
is_public: bool
|
||||
created_by: UUID (FK to users)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
```
|
||||
|
||||
### Tag
|
||||
|
||||
```python
|
||||
class Tag(Base):
|
||||
id: UUID
|
||||
name: str (unique)
|
||||
color: str | None
|
||||
description: str | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
```
|
||||
|
||||
### AuditLog
|
||||
|
||||
```python
|
||||
class AuditLog(Base):
|
||||
id: UUID
|
||||
user_id: UUID (FK, nullable for system events)
|
||||
action: str
|
||||
entity_type: str
|
||||
entity_id: UUID
|
||||
old_value: str | None
|
||||
new_value: str | None
|
||||
ip_address: str
|
||||
created_at: datetime
|
||||
```
|
||||
|
||||
## Query Engine Design
|
||||
|
||||
### Query Syntax
|
||||
|
||||
```
|
||||
('term1', 'term2') OR tagA AND tagB XOR url:example.com
|
||||
```
|
||||
|
||||
### Parser Architecture
|
||||
|
||||
```
|
||||
Input String
|
||||
↓
|
||||
Tokenize
|
||||
↓
|
||||
Build AST Node Tree
|
||||
↓
|
||||
Validate AST
|
||||
↓
|
||||
Serialize to JSON (for storage)
|
||||
```
|
||||
|
||||
### AST Node Types
|
||||
|
||||
| Node Type | Description |
|
||||
|-----------|-------------|
|
||||
| `TermSet` | Tuple of search terms: `('term1', 'term2')` |
|
||||
| `TagFilter` | Tag-based filter: `tagA` |
|
||||
| `FieldFilter` | Field value filter: `url:example.com` |
|
||||
| `AND` | Set intersection |
|
||||
| `OR` | Set union |
|
||||
| `XOR` | Set difference |
|
||||
| `NOT` | Negation |
|
||||
|
||||
### Executor Flow
|
||||
|
||||
```
|
||||
1. Parse query expression
|
||||
2. Validate AST
|
||||
3. Build SQL from AST
|
||||
4. Execute against PostgreSQL
|
||||
5. Return result set
|
||||
6. Serialize for client
|
||||
```
|
||||
|
||||
### Full-Text Search
|
||||
|
||||
PostgreSQL full-text search enabled via:
|
||||
|
||||
```sql
|
||||
CREATE INDEX links_fts_idx ON links USING GIN (to_tsvector('english', url || ' ' || title || ' ' || description || ' ' || notes));
|
||||
```
|
||||
|
||||
Query terms converted to tsquery and matched.
|
||||
|
||||
## API Design
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
```
|
||||
1. POST /api/auth/register/ - Create new account
|
||||
2. POST /api/auth/login/ - Get JWT token
|
||||
3. Include Authorization header in all API requests
|
||||
4. Token validated per request
|
||||
```
|
||||
|
||||
### Link Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | /api/links/ | List links (paginated) |
|
||||
| GET | /api/links/{id}/ | Get link details |
|
||||
| POST | /api/links/ | Create link |
|
||||
| PUT | /api/links/{id}/ | Update link |
|
||||
| DELETE | /api/links/{id}/ | Delete link |
|
||||
|
||||
### Collection Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | /api/collections/ | List collections |
|
||||
| GET | /api/collections/{id}/ | Get collection |
|
||||
| POST | /api/collections/ | Create collection |
|
||||
| PUT | /api/collections/{id}/ | Update collection |
|
||||
| DELETE | /api/collections/{id}/ | Delete collection |
|
||||
| POST | /api/collections/{id}/refresh/ | Re-evaluate dynamic |
|
||||
|
||||
### Query Execution Endpoint
|
||||
|
||||
```
|
||||
POST /api/queries/execute/
|
||||
{
|
||||
"expression": "('work', 'dev') OR tag:work",
|
||||
"static_collections": [...]
|
||||
}
|
||||
→ Returns filtered link list
|
||||
```
|
||||
|
||||
## Security Design
|
||||
|
||||
### Password Storage
|
||||
|
||||
- bcrypt hashing with cost factor 12
|
||||
- Passwords never logged or exposed
|
||||
|
||||
### JWT Tokens
|
||||
|
||||
- HS256 algorithm
|
||||
- 24-hour expiration
|
||||
- Refresh token pattern for long sessions
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
- 100 requests per minute per IP
|
||||
- 10 login attempts per hour per IP
|
||||
|
||||
### CORS
|
||||
|
||||
- Configurable origin whitelist
|
||||
- Credentials allowed for extension
|
||||
|
||||
## Docker Compose Design
|
||||
|
||||
### Services
|
||||
|
||||
| Service | Image | Purpose |
|
||||
|---------|-------|---------|
|
||||
| web | built from Dockerfile | FastAPI app |
|
||||
| db | postgres:15-alpine | PostgreSQL database |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```
|
||||
DATABASE_URL=postgresql://linksync:password@db:5432/linksync
|
||||
SECRET_KEY=<generated>
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=<password>
|
||||
DEBUG=False
|
||||
HOST=0.0.0.0
|
||||
PORT=5000
|
||||
CORS_ORIGINS=http://localhost:5555
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
```
|
||||
|
||||
## Sync Protocol Design
|
||||
|
||||
### Sync Endpoint
|
||||
|
||||
```
|
||||
POST /api/sync/
|
||||
|
||||
Request:
|
||||
{
|
||||
"type": "bi-directional|browser-authoritative|server-authoritative",
|
||||
"deletions_enabled": true,
|
||||
"links": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"url": "https://...",
|
||||
"title": "...",
|
||||
... all fields
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"actions": [
|
||||
{"type": "create", "link_id": "..."},
|
||||
{"type": "update", "link_id": "..."},
|
||||
{"type": "delete", "link_id": "..."}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Conflict Resolution
|
||||
|
||||
Priority based on sync mode:
|
||||
|
||||
1. **Bi-directional**: Keep both versions, merge metadata
|
||||
2. **Browser Authoritative**: Overwrite with browser data
|
||||
3. **Server Authoritative**: Download only, no overwrites
|
||||
|
||||
## Template Design
|
||||
|
||||
### Layout Components
|
||||
|
||||
```html
|
||||
<!-- base.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}LinkSync{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav>{% block nav %}{% endblock %}</nav>
|
||||
<main>{% block content %}{% endblock %}</main>
|
||||
<footer>{% block footer %}{% endblock %}</footer>
|
||||
<script src="/static/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Responsive Design
|
||||
|
||||
- Mobile-first CSS
|
||||
- Breakpoints: 768px, 1024px
|
||||
- Touch-friendly UI controls
|
||||
Reference in New Issue
Block a user