@bernierllc/email 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +112 -0
- package/.flake8 +18 -0
- package/.github/workflows/ci.yml +300 -0
- package/EXTRACTION_SUMMARY.md +265 -0
- package/IMPLEMENTATION_STATUS.md +159 -0
- package/LICENSE +7 -0
- package/OPEN_SOURCE_SETUP.md +420 -0
- package/PACKAGE_USAGE.md +471 -0
- package/README.md +232 -0
- package/examples/fastapi-example/main.py +257 -0
- package/examples/nextjs-example/next-env.d.ts +13 -0
- package/examples/nextjs-example/package.json +26 -0
- package/examples/nextjs-example/pages/admin/templates.tsx +157 -0
- package/examples/nextjs-example/tsconfig.json +28 -0
- package/package.json +32 -0
- package/packages/core/package.json +70 -0
- package/packages/core/rollup.config.js +37 -0
- package/packages/core/specification.md +416 -0
- package/packages/core/src/adapters/supabase.ts +291 -0
- package/packages/core/src/core/scheduler.ts +356 -0
- package/packages/core/src/core/template-manager.ts +388 -0
- package/packages/core/src/index.ts +30 -0
- package/packages/core/src/providers/base.ts +104 -0
- package/packages/core/src/providers/sendgrid.ts +368 -0
- package/packages/core/src/types/provider.ts +91 -0
- package/packages/core/src/types/scheduled.ts +78 -0
- package/packages/core/src/types/template.ts +97 -0
- package/packages/core/tsconfig.json +23 -0
- package/packages/python/README.md +106 -0
- package/packages/python/email_template_manager/__init__.py +66 -0
- package/packages/python/email_template_manager/config.py +98 -0
- package/packages/python/email_template_manager/core/magic_links.py +245 -0
- package/packages/python/email_template_manager/core/manager.py +344 -0
- package/packages/python/email_template_manager/core/scheduler.py +473 -0
- package/packages/python/email_template_manager/exceptions.py +67 -0
- package/packages/python/email_template_manager/models/magic_link.py +59 -0
- package/packages/python/email_template_manager/models/scheduled.py +78 -0
- package/packages/python/email_template_manager/models/template.py +90 -0
- package/packages/python/email_template_manager/providers/aws_ses.py +44 -0
- package/packages/python/email_template_manager/providers/base.py +94 -0
- package/packages/python/email_template_manager/providers/sendgrid.py +325 -0
- package/packages/python/email_template_manager/providers/smtp.py +44 -0
- package/packages/python/pyproject.toml +133 -0
- package/packages/python/setup.py +93 -0
- package/packages/python/specification.md +930 -0
- package/packages/react/README.md +13 -0
- package/packages/react/package.json +105 -0
- package/packages/react/rollup.config.js +37 -0
- package/packages/react/specification.md +569 -0
- package/packages/react/src/index.ts +20 -0
- package/packages/react/tsconfig.json +24 -0
- package/src/index.js +1 -0
- package/test_package.py +125 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# email-template-manager (Python SDK)
|
|
2
|
+
|
|
3
|
+
A comprehensive Python SDK for email template management, providing both low-level components and high-level integrations for FastAPI, Django, and Flask applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Basic installation
|
|
9
|
+
pip install email-template-manager
|
|
10
|
+
|
|
11
|
+
# With FastAPI support
|
|
12
|
+
pip install email-template-manager[fastapi]
|
|
13
|
+
|
|
14
|
+
# With Django support
|
|
15
|
+
pip install email-template-manager[django]
|
|
16
|
+
|
|
17
|
+
# With all providers
|
|
18
|
+
pip install email-template-manager[sendgrid,ses,mailgun]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Basic Usage
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from email_template_manager import EmailTemplateManager, SendGridProvider, EmailScheduler
|
|
27
|
+
|
|
28
|
+
# Initialize components
|
|
29
|
+
template_manager = EmailTemplateManager("postgresql://user:pass@localhost/db")
|
|
30
|
+
email_provider = SendGridProvider({
|
|
31
|
+
"api_key": "your-sendgrid-key",
|
|
32
|
+
"from_email": "noreply@yourapp.com"
|
|
33
|
+
})
|
|
34
|
+
scheduler = EmailScheduler(template_manager, email_provider)
|
|
35
|
+
|
|
36
|
+
# Create a template
|
|
37
|
+
template = await template_manager.create_template({
|
|
38
|
+
"name": "Welcome Email",
|
|
39
|
+
"subject": "Welcome {{name}}!",
|
|
40
|
+
"html_body": "<h1>Welcome {{name}}</h1><p>Thanks for joining!</p>",
|
|
41
|
+
"text_body": "Welcome {{name}}!\n\nThanks for joining!",
|
|
42
|
+
"variables": [
|
|
43
|
+
{"name": "name", "type": "text", "required": True}
|
|
44
|
+
]
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
# Schedule an email
|
|
48
|
+
scheduled = await scheduler.schedule_email({
|
|
49
|
+
"template_id": template.id,
|
|
50
|
+
"recipient_email": "user@example.com",
|
|
51
|
+
"variables": {"name": "John Doe"},
|
|
52
|
+
"trigger_type": "immediate"
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### FastAPI Integration
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from fastapi import FastAPI
|
|
60
|
+
from email_template_manager.integrations.fastapi import create_email_router
|
|
61
|
+
|
|
62
|
+
app = FastAPI()
|
|
63
|
+
|
|
64
|
+
# Add email management routes
|
|
65
|
+
email_router = create_email_router(
|
|
66
|
+
database_url="postgresql://...",
|
|
67
|
+
email_provider_config={
|
|
68
|
+
"provider": "sendgrid",
|
|
69
|
+
"api_key": "your-key",
|
|
70
|
+
"from_email": "noreply@yourapp.com"
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
app.include_router(email_router, prefix="/api")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Core Features
|
|
77
|
+
|
|
78
|
+
- **Template Management** - Create, update, and manage email templates with variables
|
|
79
|
+
- **Email Scheduling** - Schedule emails based on events or specific times
|
|
80
|
+
- **Magic Links** - Generate secure, expiring links for authentication
|
|
81
|
+
- **Multiple Providers** - Support for SendGrid, AWS SES, Mailgun, and more
|
|
82
|
+
- **Framework Integration** - Ready-to-use integrations for FastAPI, Django, Flask
|
|
83
|
+
- **Async Support** - Full async/await support throughout
|
|
84
|
+
- **Type Safety** - Complete TypeScript-style type hints
|
|
85
|
+
|
|
86
|
+
## Provider Support
|
|
87
|
+
|
|
88
|
+
- SendGrid
|
|
89
|
+
- AWS SES
|
|
90
|
+
- Mailgun
|
|
91
|
+
- Postmark
|
|
92
|
+
- Generic SMTP
|
|
93
|
+
|
|
94
|
+
## Framework Integrations
|
|
95
|
+
|
|
96
|
+
- FastAPI (async routes and background tasks)
|
|
97
|
+
- Django (views and admin integration)
|
|
98
|
+
- Flask (blueprints and extensions)
|
|
99
|
+
|
|
100
|
+
## Documentation
|
|
101
|
+
|
|
102
|
+
Full documentation available at: https://email-template-manager.readthedocs.io
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
Email Template Manager - Universal email template management library
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "1.0.0"
|
|
14
|
+
|
|
15
|
+
# Configuration
|
|
16
|
+
from .config import EmailManagerConfig
|
|
17
|
+
from .core.magic_links import MagicLinkManager
|
|
18
|
+
|
|
19
|
+
# Core classes
|
|
20
|
+
from .core.manager import EmailTemplateManager
|
|
21
|
+
from .core.scheduler import EmailScheduler
|
|
22
|
+
|
|
23
|
+
# Exceptions
|
|
24
|
+
from .exceptions import (
|
|
25
|
+
ConfigurationError,
|
|
26
|
+
DeliveryError,
|
|
27
|
+
EmailTemplateManagerError,
|
|
28
|
+
TemplateNotFoundError,
|
|
29
|
+
ValidationError,
|
|
30
|
+
)
|
|
31
|
+
from .models.magic_link import MagicLink
|
|
32
|
+
from .models.scheduled import ScheduledEmail
|
|
33
|
+
|
|
34
|
+
# Data models
|
|
35
|
+
from .models.template import EmailTemplate, TemplateVariable
|
|
36
|
+
from .providers.aws_ses import SESProvider
|
|
37
|
+
|
|
38
|
+
# Email providers
|
|
39
|
+
from .providers.base import EmailProvider
|
|
40
|
+
from .providers.sendgrid import SendGridProvider
|
|
41
|
+
from .providers.smtp import SMTPProvider
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# Core classes
|
|
45
|
+
"EmailTemplateManager",
|
|
46
|
+
"EmailScheduler",
|
|
47
|
+
"MagicLinkManager",
|
|
48
|
+
# Models
|
|
49
|
+
"EmailTemplate",
|
|
50
|
+
"TemplateVariable",
|
|
51
|
+
"ScheduledEmail",
|
|
52
|
+
"MagicLink",
|
|
53
|
+
# Providers
|
|
54
|
+
"EmailProvider",
|
|
55
|
+
"SendGridProvider",
|
|
56
|
+
"SESProvider",
|
|
57
|
+
"SMTPProvider",
|
|
58
|
+
# Configuration
|
|
59
|
+
"EmailManagerConfig",
|
|
60
|
+
# Exceptions
|
|
61
|
+
"EmailTemplateManagerError",
|
|
62
|
+
"TemplateNotFoundError",
|
|
63
|
+
"ValidationError",
|
|
64
|
+
"DeliveryError",
|
|
65
|
+
"ConfigurationError",
|
|
66
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
Configuration models for email template manager
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DatabaseConfig(BaseModel):
|
|
19
|
+
"""Database configuration"""
|
|
20
|
+
|
|
21
|
+
url: str
|
|
22
|
+
echo: bool = False
|
|
23
|
+
pool_size: int = 5
|
|
24
|
+
max_overflow: int = 10
|
|
25
|
+
pool_timeout: int = 30
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EmailProviderConfig(BaseModel):
|
|
29
|
+
"""Email provider configuration"""
|
|
30
|
+
|
|
31
|
+
provider: Literal["sendgrid", "ses", "mailgun", "postmark", "smtp"]
|
|
32
|
+
api_key: Optional[str] = None
|
|
33
|
+
region: Optional[str] = None
|
|
34
|
+
endpoint: Optional[str] = None
|
|
35
|
+
from_email: str
|
|
36
|
+
from_name: Optional[str] = None
|
|
37
|
+
reply_to: Optional[str] = None
|
|
38
|
+
webhook_url: Optional[str] = None
|
|
39
|
+
webhook_secret: Optional[str] = None
|
|
40
|
+
sandbox: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TemplateConfig(BaseModel):
|
|
44
|
+
"""Template configuration"""
|
|
45
|
+
|
|
46
|
+
default_from_email: str
|
|
47
|
+
default_from_name: Optional[str] = None
|
|
48
|
+
liquid_engine: Dict[str, Any] = Field(default_factory=dict)
|
|
49
|
+
cache_templates: bool = True
|
|
50
|
+
cache_ttl_seconds: int = 3600
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SchedulingConfig(BaseModel):
|
|
54
|
+
"""Scheduling configuration"""
|
|
55
|
+
|
|
56
|
+
batch_size: int = 50
|
|
57
|
+
default_max_retries: int = 3
|
|
58
|
+
retry_delay_minutes: List[int] = Field(default_factory=lambda: [5, 15, 60])
|
|
59
|
+
processing_interval_minutes: int = 5
|
|
60
|
+
max_concurrent_sends: int = 10
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SecurityConfig(BaseModel):
|
|
64
|
+
"""Security configuration"""
|
|
65
|
+
|
|
66
|
+
encryption_key: str
|
|
67
|
+
token_length: int = 32
|
|
68
|
+
default_expiry_hours: int = 24
|
|
69
|
+
hash_algorithm: str = "sha256"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LoggingConfig(BaseModel):
|
|
73
|
+
"""Logging configuration"""
|
|
74
|
+
|
|
75
|
+
level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
|
|
76
|
+
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
77
|
+
include_payload: bool = False
|
|
78
|
+
log_to_file: bool = False
|
|
79
|
+
log_file_path: Optional[str] = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class EmailManagerConfig(BaseModel):
|
|
83
|
+
"""Main email manager configuration"""
|
|
84
|
+
|
|
85
|
+
database: DatabaseConfig
|
|
86
|
+
email_provider: EmailProviderConfig
|
|
87
|
+
templates: TemplateConfig
|
|
88
|
+
scheduling: SchedulingConfig = Field(default_factory=SchedulingConfig)
|
|
89
|
+
security: SecurityConfig
|
|
90
|
+
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
|
91
|
+
|
|
92
|
+
# Optional feature flags
|
|
93
|
+
enable_webhooks: bool = True
|
|
94
|
+
enable_analytics: bool = False
|
|
95
|
+
enable_rate_limiting: bool = False
|
|
96
|
+
|
|
97
|
+
# Custom metadata
|
|
98
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
Magic link manager implementation
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import secrets
|
|
15
|
+
import uuid
|
|
16
|
+
from datetime import datetime, timedelta
|
|
17
|
+
from typing import Any, Dict, Optional
|
|
18
|
+
|
|
19
|
+
from cryptography.fernet import Fernet
|
|
20
|
+
from sqlalchemy import JSON, Boolean, Column, DateTime, String
|
|
21
|
+
|
|
22
|
+
from ..exceptions import EncryptionError
|
|
23
|
+
from ..models.magic_link import MagicLink
|
|
24
|
+
from .manager import Base, EmailTemplateManager
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MagicLinkModel(Base):
|
|
30
|
+
"""SQLAlchemy model for magic links"""
|
|
31
|
+
|
|
32
|
+
__tablename__ = "magic_links"
|
|
33
|
+
|
|
34
|
+
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
35
|
+
token = Column(String, unique=True, nullable=False)
|
|
36
|
+
email = Column(String, nullable=False)
|
|
37
|
+
type = Column(String, nullable=False)
|
|
38
|
+
payload = Column(JSON, default={})
|
|
39
|
+
expires_at = Column(DateTime, nullable=False)
|
|
40
|
+
used_at = Column(DateTime, nullable=True)
|
|
41
|
+
is_used = Column(Boolean, default=False)
|
|
42
|
+
link_metadata = Column(JSON, default={})
|
|
43
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MagicLinkManager:
|
|
47
|
+
"""Manages magic links for authentication and secure actions"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, template_manager: EmailTemplateManager, encryption_key: str):
|
|
50
|
+
"""
|
|
51
|
+
Initialize magic link manager
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
template_manager: Template manager instance
|
|
55
|
+
encryption_key: Key for encrypting/decrypting payloads
|
|
56
|
+
"""
|
|
57
|
+
self.template_manager = template_manager
|
|
58
|
+
self.fernet = Fernet(
|
|
59
|
+
encryption_key.encode()
|
|
60
|
+
if isinstance(encryption_key, str)
|
|
61
|
+
else encryption_key
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Ensure tables are created
|
|
65
|
+
Base.metadata.create_all(self.template_manager.engine)
|
|
66
|
+
|
|
67
|
+
async def generate_magic_link(
|
|
68
|
+
self,
|
|
69
|
+
email: str,
|
|
70
|
+
link_type: str,
|
|
71
|
+
payload: Dict[str, Any],
|
|
72
|
+
expires_in_hours: int = 24,
|
|
73
|
+
) -> MagicLink:
|
|
74
|
+
"""Generate a new magic link"""
|
|
75
|
+
try:
|
|
76
|
+
# Generate secure token
|
|
77
|
+
token = secrets.token_urlsafe(32)
|
|
78
|
+
|
|
79
|
+
# Encrypt payload
|
|
80
|
+
encrypted_payload = self._encrypt_payload(payload)
|
|
81
|
+
|
|
82
|
+
# Calculate expiry
|
|
83
|
+
expires_at = datetime.utcnow() + timedelta(hours=expires_in_hours)
|
|
84
|
+
|
|
85
|
+
with self.template_manager.get_session() as session:
|
|
86
|
+
magic_link_model = MagicLinkModel(
|
|
87
|
+
id=str(uuid.uuid4()),
|
|
88
|
+
token=token,
|
|
89
|
+
email=email,
|
|
90
|
+
type=link_type,
|
|
91
|
+
payload=encrypted_payload,
|
|
92
|
+
expires_at=expires_at,
|
|
93
|
+
is_used=False,
|
|
94
|
+
link_metadata={},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
session.add(magic_link_model)
|
|
98
|
+
session.commit()
|
|
99
|
+
session.refresh(magic_link_model)
|
|
100
|
+
|
|
101
|
+
return self._model_to_magic_link(magic_link_model)
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Failed to generate magic link: {str(e)}")
|
|
105
|
+
raise EncryptionError(f"Failed to generate magic link: {str(e)}")
|
|
106
|
+
|
|
107
|
+
async def validate_magic_link(self, token: str) -> Optional[MagicLink]:
|
|
108
|
+
"""Validate a magic link token"""
|
|
109
|
+
try:
|
|
110
|
+
with self.template_manager.get_session() as session:
|
|
111
|
+
magic_link_model = (
|
|
112
|
+
session.query(MagicLinkModel)
|
|
113
|
+
.filter(MagicLinkModel.token == token)
|
|
114
|
+
.first()
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if not magic_link_model:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
# Check if expired
|
|
121
|
+
if magic_link_model.expires_at < datetime.utcnow():
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
# Check if already used
|
|
125
|
+
if magic_link_model.is_used:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
return self._model_to_magic_link(magic_link_model)
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Failed to validate magic link: {str(e)}")
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
async def use_magic_link(self, token: str) -> Optional[MagicLink]:
|
|
135
|
+
"""Use a magic link (marks it as used)"""
|
|
136
|
+
try:
|
|
137
|
+
magic_link = await self.validate_magic_link(token)
|
|
138
|
+
if not magic_link:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
with self.template_manager.get_session() as session:
|
|
142
|
+
magic_link_model = (
|
|
143
|
+
session.query(MagicLinkModel)
|
|
144
|
+
.filter(MagicLinkModel.token == token)
|
|
145
|
+
.first()
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if magic_link_model:
|
|
149
|
+
magic_link_model.is_used = True # type: ignore
|
|
150
|
+
magic_link_model.used_at = datetime.utcnow() # type: ignore
|
|
151
|
+
session.commit()
|
|
152
|
+
|
|
153
|
+
return self._model_to_magic_link(magic_link_model)
|
|
154
|
+
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error(f"Failed to use magic link: {str(e)}")
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
async def revoke_magic_link(self, token: str) -> bool:
|
|
162
|
+
"""Revoke a magic link"""
|
|
163
|
+
try:
|
|
164
|
+
with self.template_manager.get_session() as session:
|
|
165
|
+
magic_link_model = (
|
|
166
|
+
session.query(MagicLinkModel)
|
|
167
|
+
.filter(MagicLinkModel.token == token)
|
|
168
|
+
.first()
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if magic_link_model:
|
|
172
|
+
magic_link_model.is_used = True # type: ignore
|
|
173
|
+
magic_link_model.used_at = datetime.utcnow() # type: ignore
|
|
174
|
+
session.commit()
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.error(f"Failed to revoke magic link: {str(e)}")
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
async def cleanup_expired_links(self) -> int:
|
|
184
|
+
"""Remove expired magic links"""
|
|
185
|
+
try:
|
|
186
|
+
with self.template_manager.get_session() as session:
|
|
187
|
+
expired_count = (
|
|
188
|
+
session.query(MagicLinkModel)
|
|
189
|
+
.filter(MagicLinkModel.expires_at < datetime.utcnow())
|
|
190
|
+
.count()
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
session.query(MagicLinkModel).filter(
|
|
194
|
+
MagicLinkModel.expires_at < datetime.utcnow()
|
|
195
|
+
).delete()
|
|
196
|
+
|
|
197
|
+
session.commit()
|
|
198
|
+
return expired_count
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.error(f"Failed to cleanup expired links: {str(e)}")
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
def _encrypt_payload(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
205
|
+
"""Encrypt sensitive payload data"""
|
|
206
|
+
try:
|
|
207
|
+
import json
|
|
208
|
+
|
|
209
|
+
payload_json = json.dumps(payload)
|
|
210
|
+
encrypted_data = self.fernet.encrypt(payload_json.encode())
|
|
211
|
+
return {"encrypted": encrypted_data.decode()}
|
|
212
|
+
except Exception as e:
|
|
213
|
+
raise EncryptionError(f"Failed to encrypt payload: {str(e)}")
|
|
214
|
+
|
|
215
|
+
def _decrypt_payload(self, encrypted_payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
216
|
+
"""Decrypt payload data"""
|
|
217
|
+
try:
|
|
218
|
+
import json
|
|
219
|
+
|
|
220
|
+
encrypted_data = encrypted_payload.get("encrypted", "")
|
|
221
|
+
decrypted_data = self.fernet.decrypt(encrypted_data.encode())
|
|
222
|
+
return json.loads(decrypted_data.decode())
|
|
223
|
+
except Exception as e:
|
|
224
|
+
raise EncryptionError(f"Failed to decrypt payload: {str(e)}")
|
|
225
|
+
|
|
226
|
+
def _model_to_magic_link(self, model: MagicLinkModel) -> MagicLink:
|
|
227
|
+
"""Convert SQLAlchemy model to Pydantic model"""
|
|
228
|
+
# Decrypt payload
|
|
229
|
+
try:
|
|
230
|
+
decrypted_payload = self._decrypt_payload(model.payload) # type: ignore
|
|
231
|
+
except Exception:
|
|
232
|
+
decrypted_payload = {}
|
|
233
|
+
|
|
234
|
+
return MagicLink(
|
|
235
|
+
id=model.id, # type: ignore
|
|
236
|
+
token=model.token, # type: ignore
|
|
237
|
+
email=model.email, # type: ignore
|
|
238
|
+
type=model.type, # type: ignore
|
|
239
|
+
payload=decrypted_payload,
|
|
240
|
+
expires_at=model.expires_at, # type: ignore
|
|
241
|
+
used_at=model.used_at, # type: ignore
|
|
242
|
+
is_used=model.is_used, # type: ignore
|
|
243
|
+
metadata=model.link_metadata, # type: ignore
|
|
244
|
+
created_at=model.created_at, # type: ignore
|
|
245
|
+
)
|