Compare commits
177 Commits
feature/re
...
feature/ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3aa7199503 | ||
|
|
9848268d34 | ||
|
|
9a013ad968 | ||
|
|
40067a3aa5 | ||
|
|
92019aac7e | ||
|
|
a80b35a806 | ||
|
|
af001ddaeb | ||
|
|
e52b56d51c | ||
|
|
94e37a2522 | ||
|
|
725a3c5d84 | ||
|
|
6a6ad63e7b | ||
|
|
73d2bee01a | ||
|
|
0a4a8c7687 | ||
|
|
79b76bf2dc | ||
|
|
cfb626b595 | ||
|
|
c333010620 | ||
|
|
acd84dfa30 | ||
|
|
cf8fdfccb4 | ||
|
|
89fa8f81af | ||
|
|
fbefccf436 | ||
|
|
e7733449dd | ||
|
|
29bcb27e76 | ||
|
|
41caccd31a | ||
|
|
aa9d920612 | ||
|
|
b384d9912a | ||
|
|
d25c578e59 | ||
|
|
a8c271b5e3 | ||
|
|
6afa3d7415 | ||
|
|
17786c5ec0 | ||
|
|
4a66246708 | ||
|
|
76c0d71aa0 | ||
|
|
384fe0fd86 | ||
|
|
4afcaa2b0d | ||
|
|
8c52d6a275 | ||
|
|
18c9a69d75 | ||
|
|
30ec150d90 | ||
|
|
ba2c656243 | ||
|
|
485f86086b | ||
|
|
2f6ea82114 | ||
|
|
507222316c | ||
|
|
c5c108c76f | ||
|
|
90fa628cb5 | ||
|
|
7f389830f8 | ||
|
|
30909f3268 | ||
|
|
df45a6f5d7 | ||
|
|
156ad09232 | ||
|
|
8dc2248f1f | ||
|
|
c220612214 | ||
|
|
33137289ef | ||
|
|
b2be35bdfa | ||
|
|
a4b23e44b6 | ||
|
|
67ce2c433c | ||
|
|
1391374d45 | ||
|
|
8440ac945a | ||
|
|
f4332153f4 | ||
|
|
b9e90e6f46 | ||
|
|
1af79cc019 | ||
|
|
156cc2676d | ||
|
|
897a336d0b | ||
|
|
410b46a896 | ||
|
|
01020861c7 | ||
|
|
61882b300f | ||
|
|
46b154e957 | ||
|
|
023ea7f020 | ||
|
|
35f4301fe1 | ||
|
|
6feaa8dda5 | ||
|
|
f084e33621 | ||
|
|
db0165dc5e | ||
|
|
af891d7e8f | ||
|
|
7ef255a5f1 | ||
|
|
29e99631c9 | ||
|
|
2d7c1dcd27 | ||
|
|
8d0cc1e90a | ||
|
|
cf91bae24f | ||
|
|
c7308ad167 | ||
|
|
7da5d55831 | ||
|
|
3bc8167649 | ||
|
|
b0512a660c | ||
|
|
65faaae864 | ||
|
|
dbe91ec2ff | ||
|
|
a2f74ee769 | ||
|
|
9073970189 | ||
|
|
6554e62d30 | ||
|
|
bd6d9144ce | ||
|
|
ad04e5f6ff | ||
|
|
460bf200d0 | ||
|
|
3e8634b370 | ||
|
|
bc094f2f80 | ||
|
|
c7f241b30a | ||
|
|
902582f4ba | ||
|
|
7b18637b1e | ||
|
|
3a1b2f2dd8 | ||
|
|
88b54ef9e4 | ||
|
|
5cdbc19517 | ||
|
|
f3a0f1f07a | ||
|
|
f3951295ac | ||
|
|
9cbf19ed1b | ||
|
|
88c74398e4 | ||
|
|
86947ab206 | ||
|
|
7cc013eaf2 | ||
|
|
a723d784cd | ||
|
|
13441d88fc | ||
|
|
b20fa5cfd8 | ||
|
|
093f6d9a62 | ||
|
|
5bf2fc5319 | ||
|
|
33e4b6b9b5 | ||
|
|
434f874963 | ||
|
|
0d3c97ea5f | ||
|
|
567fe0604a | ||
|
|
5244e16279 | ||
|
|
55cb97ca0d | ||
|
|
a170d6134b | ||
|
|
d2c4cbe183 | ||
|
|
47f1a4d7b4 | ||
|
|
b455be0ac6 | ||
|
|
abf67a36ed | ||
|
|
4f515c3710 | ||
|
|
fd751f02f8 | ||
|
|
04bb9e3c14 | ||
|
|
39a376b39b | ||
|
|
85c4b835fd | ||
|
|
bed0ba9304 | ||
|
|
dcb14503a2 | ||
|
|
9444e26924 | ||
|
|
445b2bb3fc | ||
|
|
baffe7e577 | ||
|
|
5aa49399d0 | ||
|
|
11bb83a85d | ||
|
|
5cef01ad0d | ||
|
|
ef58e9fc94 | ||
|
|
08b51d1a5f | ||
|
|
dc3210927a | ||
|
|
42988c0f88 | ||
|
|
e4ad7fca87 | ||
|
|
05ebd0f2bb | ||
|
|
8038f67183 | ||
|
|
ee6cf2b802 | ||
|
|
c82c60a562 | ||
|
|
06e0ec3d01 | ||
|
|
ae74b4c2ed | ||
|
|
65da1c73d0 | ||
|
|
5147101c7c | ||
|
|
10afe61bb8 | ||
|
|
f16ccf76a8 | ||
|
|
86cde135a9 | ||
|
|
7e151a23cc | ||
|
|
63723906d0 | ||
|
|
99adeda83c | ||
|
|
2b4104a819 | ||
|
|
fa3195b3b3 | ||
|
|
980b5d36aa | ||
|
|
5cd689af0a | ||
|
|
b3e2c1f324 | ||
|
|
92724d03b6 | ||
|
|
2ec78a5237 | ||
|
|
a274d70cec | ||
|
|
be3b5b2d08 | ||
|
|
89f2b570b3 | ||
|
|
885d8bbba2 | ||
|
|
c0c037e3b9 | ||
|
|
52dde7c95b | ||
|
|
af92a5ebf4 | ||
|
|
3ea71408db | ||
|
|
60708a6417 | ||
|
|
349a54e264 | ||
|
|
c8c0669801 | ||
|
|
fa68b4a869 | ||
|
|
b958f9368b | ||
|
|
2b321aef57 | ||
|
|
0d1a3045fb | ||
|
|
2b28fc49c9 | ||
|
|
4cd6610f2a | ||
|
|
f1d4dac9d2 | ||
|
|
25db8dd35a | ||
|
|
9eb07a87e6 | ||
|
|
613acf17c1 | ||
|
|
3ddd762d74 |
12
.claude/settings.local.json
Normal file
12
.claude/settings.local.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"WebSearch"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
125
.gemini/tmp/update_email_template.py
Normal file
125
.gemini/tmp/update_email_template.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
from schedule.models import EmailTemplate
|
||||||
|
import json
|
||||||
|
|
||||||
|
html_content = """
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f5f5; padding: 40px 20px;">
|
||||||
|
<tbody><tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 6px; box-shadow: 0 4px 6px rgba(0,0,0,0.05), 0 1px 3px rgba(0,0,0,0.08);">
|
||||||
|
<!-- Header -->
|
||||||
|
<tbody><tr>
|
||||||
|
<td style="background-color: #4f46e5; padding: 30px; text-align: center; border-radius: 6px 6px 0 0;">
|
||||||
|
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 600;">Appointment Confirmed</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px;">
|
||||||
|
<p style="margin: 0 0 20px 0; color: #374151; font-size: 16px; line-height: 1.6;">
|
||||||
|
Hello <strong>{{CUSTOMER_NAME}}</strong>,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin: 0 0 30px 0; color: #374151; font-size: 16px; line-height: 1.6;">
|
||||||
|
Your appointment has been confirmed. We look forward to seeing you!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Appointment Details Card -->
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f5f5; border-radius: 6px; border: 1px solid #e5e7eb; margin-bottom: 30px;">
|
||||||
|
<tbody><tr>
|
||||||
|
<td style="padding: 20px;">
|
||||||
|
<table width="100%" cellpadding="8" cellspacing="0">
|
||||||
|
<tbody><tr>
|
||||||
|
<td style="color: #6b7280; font-size: 14px; font-weight: 600;">Service:</td>
|
||||||
|
<td style="color: #111827; font-size: 14px; text-align: right;">{{SERVICE_NAME}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="color: #6b7280; font-size: 14px; font-weight: 600;">Date & Time:</td>
|
||||||
|
<td style="color: #111827; font-size: 14px; text-align: right;">{{EVENT_START_DATETIME}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="color: #6b7280; font-size: 14px; font-weight: 600;">Duration:</td>
|
||||||
|
<td style="color: #111827; font-size: 14px; text-align: right;">{{SERVICE_DURATION}} minutes</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="color: #6b7280; font-size: 14px; font-weight: 600;">With:</td>
|
||||||
|
<td style="color: #111827; font-size: 14px; text-align: right;">{{STAFF_NAME}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
<!-- Call to Action Button (example - not in original but good to show professional button style) -->
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-bottom: 30px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding-top: 10px;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" bgcolor="#4f46e5" style="border-radius: 5px; background-color: #4f46e5; padding: 12px 25px;">
|
||||||
|
<a href="{{VIEW_APPOINTMENT_LINK}}" target="_blank" style="font-size: 16px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; color: #ffffff; text-decoration: none; font-weight: 600; display: inline-block;">View My Appointment</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin: 0 0 20px 0; color: #6b7280; font-size: 14px; line-height: 1.6;">
|
||||||
|
If you need to reschedule or cancel, please contact us at least 24 hours in advance.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #f9fafb; padding: 20px 30px; text-align: center; border-radius: 0 0 6px 6px; border-top: 1px solid #e5e7eb;">
|
||||||
|
<p style="margin: 0; color: #6b7280; font-size: 14px;">
|
||||||
|
<strong>{{BUSINESS_NAME}}</strong><br>
|
||||||
|
{{BUSINESS_EMAIL}} | {{BUSINESS_PHONE}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
"""
|
||||||
|
|
||||||
|
text_content = """
|
||||||
|
Hello {{CUSTOMER_NAME}},
|
||||||
|
|
||||||
|
Your appointment has been confirmed. We look forward to seeing you!
|
||||||
|
|
||||||
|
---
|
||||||
|
Appointment Details:
|
||||||
|
Service: {{SERVICE_NAME}}
|
||||||
|
Date & Time: {{EVENT_START_DATETIME}}
|
||||||
|
Duration: {{SERVICE_DURATION}} minutes
|
||||||
|
With: {{STAFF_NAME}}
|
||||||
|
---
|
||||||
|
|
||||||
|
If you need to reschedule or cancel, please contact us at least 24 hours in advance.
|
||||||
|
|
||||||
|
View your appointment: {{VIEW_APPOINTMENT_LINK}}
|
||||||
|
|
||||||
|
---
|
||||||
|
{{BUSINESS_NAME}}
|
||||||
|
{{BUSINESS_EMAIL}} | {{BUSINESS_PHONE}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_name = "Appointment Confirmed" # Assuming this is the name of the template to update
|
||||||
|
|
||||||
|
try:
|
||||||
|
template = EmailTemplate.objects.get(name=template_name)
|
||||||
|
template.html_content = html_content
|
||||||
|
template.text_content = text_content
|
||||||
|
template.save()
|
||||||
|
print(f"Successfully updated template '{template_name}'.")
|
||||||
|
except EmailTemplate.DoesNotExist:
|
||||||
|
print(f"Error: Template '{template_name}' not found.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Test coverage reports (generated)
|
||||||
|
frontend/coverage/
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.idea/misc.xml
|
||||||
|
*.imlAGENCY_COST_ESTIMATE.md
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
17
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
17
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ourVersions">
|
||||||
|
<value>
|
||||||
|
<list size="3">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="3.14" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="3.7" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="3.8" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
18
.idea/smoothschedule2.iml
generated
Normal file
18
.idea/smoothschedule2.iml
generated
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module version="4">
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
<component name="TemplatesService">
|
||||||
|
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
|
||||||
|
<option name="TEMPLATE_FOLDERS">
|
||||||
|
<list>
|
||||||
|
<option value="$MODULE_DIR$/smoothschedule/schedule/templates" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="TestRunnerService">
|
||||||
|
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
352
ANALYTICS_CHANGES.md
Normal file
352
ANALYTICS_CHANGES.md
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# Advanced Analytics Implementation - Change Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Successfully implemented the Advanced Analytics feature with permission-based access control in the Django backend. All analytics endpoints are gated behind the `advanced_analytics` permission from the subscription plan.
|
||||||
|
|
||||||
|
## Files Created
|
||||||
|
|
||||||
|
### Analytics App (`/smoothschedule/analytics/`)
|
||||||
|
|
||||||
|
1. **`__init__.py`** - Package initialization
|
||||||
|
2. **`apps.py`** - Django app configuration
|
||||||
|
3. **`admin.py`** - Admin interface (read-only app, no models)
|
||||||
|
4. **`views.py`** - AnalyticsViewSet with 3 endpoints:
|
||||||
|
- `dashboard()` - Summary statistics
|
||||||
|
- `appointments()` - Detailed appointment analytics
|
||||||
|
- `revenue()` - Revenue analytics (dual-permission gated)
|
||||||
|
5. **`serializers.py`** - Response serializers for data validation
|
||||||
|
6. **`urls.py`** - URL routing
|
||||||
|
7. **`tests.py`** - Comprehensive pytest test suite
|
||||||
|
8. **`migrations/`** - Empty migrations directory
|
||||||
|
9. **`README.md`** - Full API documentation
|
||||||
|
10. **`IMPLEMENTATION_GUIDE.md`** - Developer implementation guide
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
### 1. `/smoothschedule/core/permissions.py`
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Added `advanced_analytics` and `advanced_reporting` to the `FEATURE_NAMES` dictionary in `HasFeaturePermission`
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```python
|
||||||
|
FEATURE_NAMES = {
|
||||||
|
'can_use_sms_reminders': 'SMS Reminders',
|
||||||
|
...
|
||||||
|
'can_use_calendar_sync': 'Calendar Sync',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```python
|
||||||
|
FEATURE_NAMES = {
|
||||||
|
'can_use_sms_reminders': 'SMS Reminders',
|
||||||
|
...
|
||||||
|
'can_use_calendar_sync': 'Calendar Sync',
|
||||||
|
'advanced_analytics': 'Advanced Analytics',
|
||||||
|
'advanced_reporting': 'Advanced Reporting',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `/smoothschedule/config/urls.py`
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Added analytics URL include in the API URL patterns
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```python
|
||||||
|
# Schedule API (internal)
|
||||||
|
path("", include("schedule.urls")),
|
||||||
|
# Payments API
|
||||||
|
path("payments/", include("payments.urls")),
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```python
|
||||||
|
# Schedule API (internal)
|
||||||
|
path("", include("schedule.urls")),
|
||||||
|
# Analytics API
|
||||||
|
path("", include("analytics.urls")),
|
||||||
|
# Payments API
|
||||||
|
path("payments/", include("payments.urls")),
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. `/smoothschedule/config/settings/base.py`
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Added `analytics` app to `LOCAL_APPS`
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```python
|
||||||
|
LOCAL_APPS = [
|
||||||
|
"smoothschedule.users",
|
||||||
|
"core",
|
||||||
|
"schedule",
|
||||||
|
"payments",
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```python
|
||||||
|
LOCAL_APPS = [
|
||||||
|
"smoothschedule.users",
|
||||||
|
"core",
|
||||||
|
"schedule",
|
||||||
|
"analytics",
|
||||||
|
"payments",
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
All endpoints are located at `/api/analytics/` and require:
|
||||||
|
- Authentication via token or session
|
||||||
|
- `advanced_analytics` permission in tenant's subscription plan
|
||||||
|
|
||||||
|
### 1. Dashboard Summary
|
||||||
|
```
|
||||||
|
GET /api/analytics/analytics/dashboard/
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Total appointments (this month and all-time)
|
||||||
|
- Active resources and services count
|
||||||
|
- Upcoming appointments
|
||||||
|
- Average appointment duration
|
||||||
|
- Peak booking day and hour
|
||||||
|
|
||||||
|
### 2. Appointment Analytics
|
||||||
|
```
|
||||||
|
GET /api/analytics/analytics/appointments/
|
||||||
|
```
|
||||||
|
|
||||||
|
Query Parameters:
|
||||||
|
- `days` (default: 30)
|
||||||
|
- `status` (optional: confirmed, cancelled, no_show)
|
||||||
|
- `service_id` (optional)
|
||||||
|
- `resource_id` (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Total appointments
|
||||||
|
- Breakdown by status
|
||||||
|
- Breakdown by service and resource
|
||||||
|
- Daily breakdown
|
||||||
|
- Booking trends and rates
|
||||||
|
|
||||||
|
### 3. Revenue Analytics
|
||||||
|
```
|
||||||
|
GET /api/analytics/analytics/revenue/
|
||||||
|
```
|
||||||
|
|
||||||
|
Query Parameters:
|
||||||
|
- `days` (default: 30)
|
||||||
|
- `service_id` (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- Total revenue in cents
|
||||||
|
- Transaction count
|
||||||
|
- Average transaction value
|
||||||
|
- Revenue by service
|
||||||
|
- Daily breakdown
|
||||||
|
|
||||||
|
**Note:** Requires both `advanced_analytics` AND `can_accept_payments` permissions
|
||||||
|
|
||||||
|
## Permission Gating Implementation
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. **Request arrives at endpoint**
|
||||||
|
2. **IsAuthenticated check** - Verifies user is logged in
|
||||||
|
3. **HasFeaturePermission('advanced_analytics') check**:
|
||||||
|
- Gets tenant from request
|
||||||
|
- Calls `tenant.has_feature('advanced_analytics')`
|
||||||
|
- Checks both direct field and subscription plan JSON
|
||||||
|
4. **If permission exists** - View logic executes
|
||||||
|
5. **If permission missing** - 403 Forbidden returned with message
|
||||||
|
|
||||||
|
### Permission Check Logic
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In core/models.py - Tenant.has_feature()
|
||||||
|
def has_feature(self, permission_key):
|
||||||
|
# Check direct field on Tenant model
|
||||||
|
if hasattr(self, permission_key):
|
||||||
|
return bool(getattr(self, permission_key))
|
||||||
|
|
||||||
|
# Check subscription plan permissions JSON
|
||||||
|
if self.subscription_plan:
|
||||||
|
plan_perms = self.subscription_plan.permissions or {}
|
||||||
|
return bool(plan_perms.get(permission_key, False))
|
||||||
|
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enabling Analytics for a Plan
|
||||||
|
|
||||||
|
### Via Django Admin
|
||||||
|
1. Go to `/admin/platform_admin/subscriptionplan/`
|
||||||
|
2. Edit a plan
|
||||||
|
3. Add to "Permissions" JSON field:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"advanced_analytics": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Via Django Shell
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py shell
|
||||||
|
|
||||||
|
from platform_admin.models import SubscriptionPlan
|
||||||
|
plan = SubscriptionPlan.objects.get(name='Professional')
|
||||||
|
perms = plan.permissions or {}
|
||||||
|
perms['advanced_analytics'] = True
|
||||||
|
plan.permissions = perms
|
||||||
|
plan.save()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Permission Tests Included
|
||||||
|
|
||||||
|
The `analytics/tests.py` file includes comprehensive tests:
|
||||||
|
|
||||||
|
1. **TestAnalyticsPermissions**
|
||||||
|
- `test_analytics_requires_authentication` - 401 without auth
|
||||||
|
- `test_analytics_denied_without_permission` - 403 without permission
|
||||||
|
- `test_analytics_allowed_with_permission` - 200 with permission
|
||||||
|
- `test_dashboard_endpoint_structure` - Verify response structure
|
||||||
|
- `test_appointments_endpoint_with_filters` - Query parameters work
|
||||||
|
- `test_revenue_requires_payments_permission` - Dual permission check
|
||||||
|
- `test_multiple_permission_check` - Both checks enforced
|
||||||
|
|
||||||
|
2. **TestAnalyticsData**
|
||||||
|
- `test_dashboard_counts_appointments_correctly` - Correct counts
|
||||||
|
- `test_appointments_counts_by_status` - Status breakdown
|
||||||
|
- `test_cancellation_rate_calculation` - Rate calculation
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all analytics tests
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest analytics/tests.py -v
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest analytics/tests.py::TestAnalyticsPermissions::test_analytics_denied_without_permission -v
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest analytics/tests.py --cov=analytics
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
### 401 Unauthorized (No Authentication)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Authentication credentials were not provided."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 403 Forbidden (No Permission)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Your current plan does not include Advanced Analytics. Please upgrade your subscription to access this feature."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 403 Forbidden (Revenue Endpoint - Missing Payments Permission)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Payment analytics not available",
|
||||||
|
"detail": "Your plan does not include payment processing."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
### Get Dashboard Stats (with cURL)
|
||||||
|
```bash
|
||||||
|
TOKEN="your_auth_token_here"
|
||||||
|
|
||||||
|
curl -H "Authorization: Token $TOKEN" \
|
||||||
|
http://lvh.me:8000/api/analytics/analytics/dashboard/ | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Appointment Analytics (with filters)
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Token $TOKEN" \
|
||||||
|
"http://lvh.me:8000/api/analytics/analytics/appointments/?days=7&status=confirmed" | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Revenue Analytics
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Token $TOKEN" \
|
||||||
|
http://lvh.me:8000/api/analytics/analytics/revenue/ | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
1. **ViewSet without models** - Analytics is calculated on-the-fly, no database models
|
||||||
|
2. **Read-only endpoints** - No POST/PUT/DELETE, only GET for querying
|
||||||
|
3. **Comprehensive permission naming** - Both `advanced_analytics` and `advanced_reporting` supported for flexibility
|
||||||
|
4. **Dual permission check** - Revenue endpoint requires both analytics and payments permissions
|
||||||
|
5. **Query parameter filtering** - Flexible filtering for reports
|
||||||
|
6. **Detailed error messages** - User-friendly upgrade prompts
|
||||||
|
|
||||||
|
## Documentation Provided
|
||||||
|
|
||||||
|
1. **README.md** - Complete API documentation with examples
|
||||||
|
2. **IMPLEMENTATION_GUIDE.md** - Developer guide for enabling and debugging
|
||||||
|
3. **Code comments** - Detailed docstrings in views and serializers
|
||||||
|
4. **Test file** - Comprehensive test suite with examples
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Migrate** - No migrations needed (no database models)
|
||||||
|
2. **Configure Plans** - Add `advanced_analytics` permission to desired subscription plans
|
||||||
|
3. **Test** - Run the test suite to verify functionality
|
||||||
|
4. **Deploy** - Push to production
|
||||||
|
5. **Monitor** - Check logs for any issues
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
- [x] Create analytics app with ViewSet
|
||||||
|
- [x] Implement dashboard endpoint with summary statistics
|
||||||
|
- [x] Implement appointments endpoint with filtering
|
||||||
|
- [x] Implement revenue endpoint with dual permission check
|
||||||
|
- [x] Add permission to FEATURE_NAMES in core/permissions.py
|
||||||
|
- [x] Register app in INSTALLED_APPS
|
||||||
|
- [x] Add URL routing
|
||||||
|
- [x] Create serializers for response validation
|
||||||
|
- [x] Write comprehensive test suite
|
||||||
|
- [x] Document API endpoints
|
||||||
|
- [x] Document implementation details
|
||||||
|
- [x] Provide developer guide
|
||||||
|
|
||||||
|
## Files Summary
|
||||||
|
|
||||||
|
**Total Files Created:** 11
|
||||||
|
- 10 Python files (app code + tests)
|
||||||
|
- 2 Documentation files
|
||||||
|
|
||||||
|
**Total Files Modified:** 3
|
||||||
|
- core/permissions.py
|
||||||
|
- config/urls.py
|
||||||
|
- config/settings/base.py
|
||||||
|
|
||||||
|
**Lines of Code:**
|
||||||
|
- views.py: ~350 lines
|
||||||
|
- tests.py: ~260 lines
|
||||||
|
- serializers.py: ~80 lines
|
||||||
|
- Documentation: ~1000 lines
|
||||||
|
|
||||||
|
## Questions or Issues?
|
||||||
|
|
||||||
|
Refer to:
|
||||||
|
1. `analytics/README.md` - API usage and endpoints
|
||||||
|
2. `analytics/IMPLEMENTATION_GUIDE.md` - Setup and debugging
|
||||||
|
3. `analytics/tests.py` - Examples of correct usage
|
||||||
|
4. `core/permissions.py` - Permission checking logic
|
||||||
476
CALENDAR_SYNC_PERMISSION_IMPLEMENTATION.md
Normal file
476
CALENDAR_SYNC_PERMISSION_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
# Calendar Sync Permission Implementation
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Successfully added permission checking for the calendar sync feature in the Django backend. The implementation follows the existing `HasFeaturePermission` pattern and gates access to calendar OAuth and sync operations.
|
||||||
|
|
||||||
|
## Files Modified and Created
|
||||||
|
|
||||||
|
### Core Changes
|
||||||
|
|
||||||
|
#### 1. **core/models.py** - Tenant Model
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/core/models.py`
|
||||||
|
|
||||||
|
Added new permission field to the Tenant model:
|
||||||
|
```python
|
||||||
|
can_use_calendar_sync = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Whether this business can sync Google Calendar and other calendar providers"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- New tenants will have `can_use_calendar_sync=False` by default
|
||||||
|
- Platform admins can enable this per-tenant via the Django admin or API
|
||||||
|
- Works with existing subscription plan system
|
||||||
|
|
||||||
|
#### 2. **core/migrations/0016_tenant_can_use_calendar_sync.py** - Database Migration
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/core/migrations/0016_tenant_can_use_calendar_sync.py`
|
||||||
|
|
||||||
|
Database migration that adds the `can_use_calendar_sync` boolean field to the Tenant table.
|
||||||
|
|
||||||
|
**How to apply:**
|
||||||
|
```bash
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. **core/permissions.py** - Permission Check
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/core/permissions.py`
|
||||||
|
|
||||||
|
Updated `HasFeaturePermission` factory function:
|
||||||
|
- Added `'can_use_calendar_sync': 'Calendar Sync'` to `FEATURE_NAMES` mapping
|
||||||
|
- This displays user-friendly error messages when the feature is not available
|
||||||
|
- Follows the existing pattern used by other features (SMS reminders, webhooks, etc.)
|
||||||
|
|
||||||
|
**Usage Pattern:**
|
||||||
|
```python
|
||||||
|
from core.permissions import HasFeaturePermission
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
class MyViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_use_calendar_sync')]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. **core/oauth_views.py** - OAuth Permission Checks
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/core/oauth_views.py`
|
||||||
|
|
||||||
|
Updated OAuth views to check calendar sync permission when initiating calendar-specific OAuth flows:
|
||||||
|
|
||||||
|
**GoogleOAuthInitiateView:**
|
||||||
|
- Imported `HasFeaturePermission` from core.permissions
|
||||||
|
- Added check: If `purpose == 'calendar'`, verify tenant has `can_use_calendar_sync` permission
|
||||||
|
- Returns 403 Forbidden with upgrade message if permission denied
|
||||||
|
- Email OAuth (`purpose == 'email'`) is NOT affected by this check
|
||||||
|
|
||||||
|
**MicrosoftOAuthInitiateView:**
|
||||||
|
- Same pattern as Google OAuth
|
||||||
|
- Supports both email and calendar purposes with respective permission checks
|
||||||
|
|
||||||
|
**Docstring updates:**
|
||||||
|
Both views now document the permission requirements:
|
||||||
|
```
|
||||||
|
Permission Requirements:
|
||||||
|
- For "email" purpose: IsPlatformAdmin only
|
||||||
|
- For "calendar" purpose: Requires can_use_calendar_sync feature permission
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Calendar Sync Implementation
|
||||||
|
|
||||||
|
#### 5. **schedule/calendar_sync_views.py** - Calendar Sync Endpoints
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/calendar_sync_views.py`
|
||||||
|
|
||||||
|
Created comprehensive calendar sync views with permission checking:
|
||||||
|
|
||||||
|
**CalendarSyncPermission Custom Permission:**
|
||||||
|
- Combines authentication check with feature permission check
|
||||||
|
- Used by all calendar sync endpoints
|
||||||
|
- Ensures both user is authenticated AND tenant has permission
|
||||||
|
|
||||||
|
**CalendarListView (GET /api/calendar/list/)**
|
||||||
|
- Lists connected calendars for the current tenant
|
||||||
|
- Returns OAuth credentials with masked tokens
|
||||||
|
- Protected by CalendarSyncPermission
|
||||||
|
|
||||||
|
**CalendarSyncView (POST /api/calendar/sync/)**
|
||||||
|
- Initiates calendar event synchronization
|
||||||
|
- Accepts credential_id, calendar_id, start_date, end_date
|
||||||
|
- Verifies credential belongs to tenant
|
||||||
|
- Checks credential validity before sync
|
||||||
|
- TODO: Implement actual calendar API integration
|
||||||
|
|
||||||
|
**CalendarDeleteView (DELETE /api/calendar/disconnect/)**
|
||||||
|
- Disconnects/revokes a calendar integration
|
||||||
|
- Removes the OAuth credential
|
||||||
|
- Logs the action for audit trail
|
||||||
|
|
||||||
|
**CalendarStatusView (GET /api/calendar/status/)**
|
||||||
|
- Informational endpoint (authentication only, not feature-gated)
|
||||||
|
- Returns whether calendar sync is enabled for tenant
|
||||||
|
- Shows number of connected calendars
|
||||||
|
- User-friendly message if feature not available
|
||||||
|
|
||||||
|
#### 6. **schedule/calendar_sync_urls.py** - URL Configuration
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/calendar_sync_urls.py`
|
||||||
|
|
||||||
|
URL routes for calendar sync endpoints:
|
||||||
|
```
|
||||||
|
/api/calendar/status/ - Check calendar sync status
|
||||||
|
/api/calendar/list/ - List connected calendars
|
||||||
|
/api/calendar/sync/ - Sync calendar events
|
||||||
|
/api/calendar/disconnect/ - Disconnect a calendar
|
||||||
|
```
|
||||||
|
|
||||||
|
To integrate with main URL config, add to config/urls.py:
|
||||||
|
```python
|
||||||
|
path("calendar/", include("schedule.calendar_sync_urls", namespace="calendar")),
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. **schedule/tests/test_calendar_sync_permissions.py** - Test Suite
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/tests/test_calendar_sync_permissions.py`
|
||||||
|
|
||||||
|
Comprehensive test suite with 20+ tests covering:
|
||||||
|
|
||||||
|
**CalendarSyncPermissionTests:**
|
||||||
|
- `test_calendar_list_without_permission` - Verify 403 when disabled
|
||||||
|
- `test_calendar_sync_without_permission` - Verify 403 when disabled
|
||||||
|
- `test_oauth_calendar_initiate_without_permission` - Verify OAuth rejects calendar
|
||||||
|
- `test_calendar_list_with_permission` - Verify 200 when enabled
|
||||||
|
- `test_calendar_with_connected_credential` - Verify credential appears in list
|
||||||
|
- `test_unauthenticated_calendar_access` - Verify 401 for anonymous users
|
||||||
|
|
||||||
|
**CalendarSyncIntegrationTests:**
|
||||||
|
- `test_full_calendar_workflow` - Complete workflow (list → connect → sync → disconnect)
|
||||||
|
|
||||||
|
**TenantPermissionModelTests:**
|
||||||
|
- `test_tenant_can_use_calendar_sync_default` - Verify default False
|
||||||
|
- `test_has_feature_with_other_permissions` - Verify method works correctly
|
||||||
|
|
||||||
|
**Run tests:**
|
||||||
|
```bash
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest schedule/tests/test_calendar_sync_permissions.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8. **CALENDAR_SYNC_INTEGRATION.md** - Integration Guide
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/CALENDAR_SYNC_INTEGRATION.md`
|
||||||
|
|
||||||
|
Comprehensive developer guide including:
|
||||||
|
- Architecture overview
|
||||||
|
- Permission flow diagram
|
||||||
|
- API endpoint examples with curl commands
|
||||||
|
- Integration patterns with ViewSets
|
||||||
|
- Testing examples
|
||||||
|
- Security considerations
|
||||||
|
- Related files reference
|
||||||
|
|
||||||
|
## Permission Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User Request to Calendar Endpoint
|
||||||
|
↓
|
||||||
|
1. [Is User Authenticated?]
|
||||||
|
├─ NO → 401 Unauthorized
|
||||||
|
└─ YES ↓
|
||||||
|
2. [Request Has Tenant Context?]
|
||||||
|
├─ NO → 400 Bad Request
|
||||||
|
└─ YES ↓
|
||||||
|
3. [Does Tenant have can_use_calendar_sync?]
|
||||||
|
├─ NO → 403 Forbidden (upgrade message)
|
||||||
|
└─ YES ↓
|
||||||
|
4. [Process Request]
|
||||||
|
├─ Success → 200 OK
|
||||||
|
└─ Error → 500 Server Error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Permission Field Design
|
||||||
|
|
||||||
|
The `can_use_calendar_sync` field:
|
||||||
|
- Is a BooleanField on the Tenant model
|
||||||
|
- Defaults to False (disabled by default)
|
||||||
|
- Can be set per-tenant by platform admins
|
||||||
|
- Works alongside subscription_plan.permissions for more granular control
|
||||||
|
- Integrates with existing `has_feature()` method on Tenant
|
||||||
|
|
||||||
|
### How Permission Checking Works
|
||||||
|
|
||||||
|
#### In OAuth Views
|
||||||
|
```python
|
||||||
|
# Check calendar sync permission if purpose is calendar
|
||||||
|
if purpose == 'calendar':
|
||||||
|
calendar_permission = HasFeaturePermission('can_use_calendar_sync')
|
||||||
|
if not calendar_permission().has_permission(request, self):
|
||||||
|
return Response({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Your current plan does not include Calendar Sync...',
|
||||||
|
}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### In Calendar Sync Views
|
||||||
|
```python
|
||||||
|
class CalendarSyncPermission(IsAuthenticated):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if not super().has_permission(request, view):
|
||||||
|
return False
|
||||||
|
|
||||||
|
tenant = getattr(request, 'tenant', None)
|
||||||
|
if not tenant:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return tenant.has_feature('can_use_calendar_sync')
|
||||||
|
|
||||||
|
class CalendarListView(APIView):
|
||||||
|
permission_classes = [CalendarSyncPermission]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Separation of Concerns
|
||||||
|
|
||||||
|
- **Email OAuth**: Not affected by calendar sync permission (separate feature)
|
||||||
|
- **Calendar OAuth**: Requires calendar sync permission only when `purpose='calendar'`
|
||||||
|
- **Calendar Sync**: Requires calendar sync permission for all operations
|
||||||
|
- **Calendar Status**: Authentication only (informational endpoint)
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Multi-Tenancy Isolation**
|
||||||
|
- All OAuthCredential queries filter by tenant
|
||||||
|
- Users can only access their own tenant's calendars
|
||||||
|
- Credentials are not shared between tenants
|
||||||
|
|
||||||
|
2. **Token Security**
|
||||||
|
- OAuth tokens stored encrypted at rest (via Django settings)
|
||||||
|
- Tokens masked in API responses
|
||||||
|
- Token validity checked before use
|
||||||
|
|
||||||
|
3. **CSRF Protection**
|
||||||
|
- OAuth state parameter validated
|
||||||
|
- Standard Django session handling
|
||||||
|
|
||||||
|
4. **Audit Trail**
|
||||||
|
- All calendar operations logged with tenant/user info
|
||||||
|
- Sync operations logged with timestamps
|
||||||
|
- Disconnect operations logged
|
||||||
|
|
||||||
|
5. **Feature Gating**
|
||||||
|
- Permission checked at view level
|
||||||
|
- No way to bypass by direct API access
|
||||||
|
- Consistent error messages for upgrade prompts
|
||||||
|
|
||||||
|
## API Examples
|
||||||
|
|
||||||
|
### Check if Feature is Available
|
||||||
|
```bash
|
||||||
|
GET /api/calendar/status/
|
||||||
|
|
||||||
|
# Response (if enabled):
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"can_use_calendar_sync": true,
|
||||||
|
"total_connected": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Response (if disabled):
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"can_use_calendar_sync": false,
|
||||||
|
"message": "Calendar Sync feature is not available for your plan"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Initiate Calendar OAuth
|
||||||
|
```bash
|
||||||
|
POST /api/oauth/google/initiate/
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"purpose": "calendar"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Response (if permission granted):
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"authorization_url": "https://accounts.google.com/o/oauth2/auth?..."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Response (if permission denied):
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Your current plan does not include Calendar Sync. Please upgrade..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Connected Calendars
|
||||||
|
```bash
|
||||||
|
GET /api/calendar/list/
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"calendars": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"provider": "Google",
|
||||||
|
"email": "user@gmail.com",
|
||||||
|
"is_valid": true,
|
||||||
|
"is_expired": false,
|
||||||
|
"created_at": "2025-12-01T08:15:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing the Implementation
|
||||||
|
|
||||||
|
### Manual Testing via API
|
||||||
|
|
||||||
|
1. **Test without permission:**
|
||||||
|
```bash
|
||||||
|
# Create a user in a tenant without calendar sync
|
||||||
|
curl -X GET http://lvh.me:8000/api/calendar/list/ \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
|
||||||
|
# Expected: 403 Forbidden
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Test with permission:**
|
||||||
|
```bash
|
||||||
|
# Enable calendar sync on tenant
|
||||||
|
# Then try again:
|
||||||
|
curl -X GET http://lvh.me:8000/api/calendar/list/ \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
|
||||||
|
# Expected: 200 OK with calendar list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Test Suite
|
||||||
|
```bash
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
|
||||||
|
|
||||||
|
# Run all calendar permission tests
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest \
|
||||||
|
schedule/tests/test_calendar_sync_permissions.py -v
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest \
|
||||||
|
schedule/tests/test_calendar_sync_permissions.py::CalendarSyncPermissionTests::test_calendar_list_without_permission -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Django Shell Testing
|
||||||
|
```bash
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
|
||||||
|
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py shell
|
||||||
|
|
||||||
|
# In Django shell:
|
||||||
|
from core.models import Tenant
|
||||||
|
from smoothschedule.users.models import User
|
||||||
|
|
||||||
|
tenant = Tenant.objects.get(schema_name='demo')
|
||||||
|
print(tenant.has_feature('can_use_calendar_sync')) # False initially
|
||||||
|
|
||||||
|
# Enable it
|
||||||
|
tenant.can_use_calendar_sync = True
|
||||||
|
tenant.save()
|
||||||
|
|
||||||
|
print(tenant.has_feature('can_use_calendar_sync')) # True now
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Existing Systems
|
||||||
|
|
||||||
|
### Works with Subscription Plans
|
||||||
|
```python
|
||||||
|
# Tenant can get permission from subscription_plan.permissions
|
||||||
|
subscription_plan.permissions = {
|
||||||
|
'can_use_calendar_sync': True,
|
||||||
|
'can_use_webhooks': True,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Works with Platform Admin Invitations
|
||||||
|
```python
|
||||||
|
# TenantInvitation can grant this permission
|
||||||
|
invitation = TenantInvitation(
|
||||||
|
can_use_calendar_sync=True,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Works with User Role-Based Access
|
||||||
|
- Permission is at tenant level, not user level
|
||||||
|
- All users in a tenant with enabled feature can use it
|
||||||
|
- Can be further restricted by user roles if needed
|
||||||
|
|
||||||
|
## Next Steps for Full Implementation
|
||||||
|
|
||||||
|
While the permission framework is complete, the following features need implementation:
|
||||||
|
|
||||||
|
1. **Google Calendar API Integration**
|
||||||
|
- Fetch events from Google Calendar API using OAuth token
|
||||||
|
- Map Google Calendar events to Event model
|
||||||
|
- Handle recurring events
|
||||||
|
- Sync deleted events
|
||||||
|
|
||||||
|
2. **Microsoft Calendar API Integration**
|
||||||
|
- Fetch events from Microsoft Graph API
|
||||||
|
- Handle Outlook calendar format
|
||||||
|
|
||||||
|
3. **Conflict Resolution**
|
||||||
|
- Handle overlapping events from multiple calendars
|
||||||
|
- Update vs. create decision logic
|
||||||
|
|
||||||
|
4. **Bi-directional Sync**
|
||||||
|
- Push events back to calendar after scheduling
|
||||||
|
- Handle edit/delete synchronization
|
||||||
|
|
||||||
|
5. **UI/Frontend Integration**
|
||||||
|
- Calendar selection dialog
|
||||||
|
- Sync status display
|
||||||
|
- Calendar disconnect confirmation
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If needed to rollback:
|
||||||
|
|
||||||
|
1. **Revert database migration:**
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py migrate core 0015_tenant_can_create_plugins_tenant_can_use_webhooks
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Revert code changes:**
|
||||||
|
- Remove lines from core/models.py (can_use_calendar_sync field)
|
||||||
|
- Remove calendar check from oauth_views.py
|
||||||
|
- Remove calendar_sync_views.py
|
||||||
|
- Remove calendar_sync_urls.py
|
||||||
|
|
||||||
|
3. **Revert permissions.py:**
|
||||||
|
- Remove 'can_use_calendar_sync' from FEATURE_NAMES
|
||||||
|
|
||||||
|
## Summary of Changes
|
||||||
|
|
||||||
|
| File | Type | Change |
|
||||||
|
|------|------|--------|
|
||||||
|
| core/models.py | Modified | Added can_use_calendar_sync field to Tenant |
|
||||||
|
| core/migrations/0016_tenant_can_use_calendar_sync.py | New | Database migration |
|
||||||
|
| core/permissions.py | Modified | Added can_use_calendar_sync to FEATURE_NAMES |
|
||||||
|
| core/oauth_views.py | Modified | Added permission check for calendar OAuth |
|
||||||
|
| schedule/calendar_sync_views.py | New | Calendar sync API views |
|
||||||
|
| schedule/calendar_sync_urls.py | New | Calendar sync URL configuration |
|
||||||
|
| schedule/tests/test_calendar_sync_permissions.py | New | Test suite (20+ tests) |
|
||||||
|
| CALENDAR_SYNC_INTEGRATION.md | New | Integration guide |
|
||||||
|
|
||||||
|
## File Locations
|
||||||
|
|
||||||
|
All files are located in: `/home/poduck/Desktop/smoothschedule2/smoothschedule/`
|
||||||
|
|
||||||
|
**Key files:**
|
||||||
|
- Models: `core/models.py` (line 194-197)
|
||||||
|
- Migration: `core/migrations/0016_tenant_can_use_calendar_sync.py`
|
||||||
|
- Permissions: `core/permissions.py` (line 354)
|
||||||
|
- OAuth Views: `core/oauth_views.py` (lines 27, 92-98, 241-247)
|
||||||
|
- Calendar Views: `schedule/calendar_sync_views.py` (entire file)
|
||||||
|
- Calendar URLs: `schedule/calendar_sync_urls.py` (entire file)
|
||||||
|
- Tests: `schedule/tests/test_calendar_sync_permissions.py` (entire file)
|
||||||
|
- Documentation: `CALENDAR_SYNC_INTEGRATION.md`
|
||||||
513
CLAUDE.md
513
CLAUDE.md
@@ -21,6 +21,169 @@
|
|||||||
|
|
||||||
Note: `lvh.me` resolves to `127.0.0.1` - required for subdomain cookies to work.
|
Note: `lvh.me` resolves to `127.0.0.1` - required for subdomain cookies to work.
|
||||||
|
|
||||||
|
## CRITICAL: Test-Driven Development (TDD) Required
|
||||||
|
|
||||||
|
**All code changes MUST follow TDD.** This is non-negotiable.
|
||||||
|
|
||||||
|
### TDD Workflow
|
||||||
|
|
||||||
|
1. **Write tests FIRST** before writing any implementation code
|
||||||
|
2. **Run tests** to verify they fail (red)
|
||||||
|
3. **Write minimal code** to make tests pass (green)
|
||||||
|
4. **Refactor** while keeping tests green
|
||||||
|
5. **Repeat** for each new feature or bug fix
|
||||||
|
|
||||||
|
### Coverage Requirements
|
||||||
|
|
||||||
|
| Target | Minimum | Goal |
|
||||||
|
|--------|---------|------|
|
||||||
|
| Backend (Django) | **80%** | 100% |
|
||||||
|
| Frontend (React) | **80%** | 100% |
|
||||||
|
|
||||||
|
### Running Tests with Coverage
|
||||||
|
|
||||||
|
**Backend (Django):**
|
||||||
|
```bash
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
|
||||||
|
|
||||||
|
# Run all tests with coverage
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest --cov --cov-report=term-missing
|
||||||
|
|
||||||
|
# Run tests for a specific app
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest smoothschedule/scheduling/schedule/tests/ --cov=smoothschedule/scheduling/schedule
|
||||||
|
|
||||||
|
# Run a single test file
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest smoothschedule/path/to/test_file.py -v
|
||||||
|
|
||||||
|
# Run tests matching a pattern
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest -k "test_create_resource" -v
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend (React):**
|
||||||
|
```bash
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2/frontend
|
||||||
|
|
||||||
|
# Run all tests with coverage
|
||||||
|
npm test -- --coverage
|
||||||
|
|
||||||
|
# Run tests in watch mode during development
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run a single test file
|
||||||
|
npm test -- src/hooks/__tests__/useResources.test.ts
|
||||||
|
|
||||||
|
# Run tests matching a pattern
|
||||||
|
npm test -- -t "should create resource"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test File Organization
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
```
|
||||||
|
smoothschedule/smoothschedule/{domain}/{app}/
|
||||||
|
├── models.py
|
||||||
|
├── views.py
|
||||||
|
├── serializers.py
|
||||||
|
└── tests/
|
||||||
|
├── __init__.py
|
||||||
|
├── test_models.py # Model unit tests
|
||||||
|
├── test_serializers.py # Serializer tests
|
||||||
|
├── test_views.py # API endpoint tests
|
||||||
|
└── factories.py # Test factories (optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
```
|
||||||
|
frontend/src/
|
||||||
|
├── hooks/
|
||||||
|
│ ├── useResources.ts
|
||||||
|
│ └── __tests__/
|
||||||
|
│ └── useResources.test.ts
|
||||||
|
├── components/
|
||||||
|
│ ├── MyComponent.tsx
|
||||||
|
│ └── __tests__/
|
||||||
|
│ └── MyComponent.test.tsx
|
||||||
|
└── pages/
|
||||||
|
├── MyPage.tsx
|
||||||
|
└── __tests__/
|
||||||
|
└── MyPage.test.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### What to Test
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- Model methods and properties
|
||||||
|
- Model validation (clean methods)
|
||||||
|
- Serializer validation
|
||||||
|
- API endpoints (all HTTP methods)
|
||||||
|
- Permission classes
|
||||||
|
- Custom querysets and managers
|
||||||
|
- Signals
|
||||||
|
- Celery tasks
|
||||||
|
- Utility functions
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- Custom hooks (state changes, API calls)
|
||||||
|
- Component rendering
|
||||||
|
- User interactions (clicks, form submissions)
|
||||||
|
- Conditional rendering
|
||||||
|
- Error states
|
||||||
|
- Loading states
|
||||||
|
- API client functions
|
||||||
|
|
||||||
|
### TDD Example - Adding a New Feature
|
||||||
|
|
||||||
|
**Step 1: Write the test first**
|
||||||
|
```python
|
||||||
|
# Backend: test_views.py
|
||||||
|
def test_create_resource_with_schedule(self, api_client, tenant):
|
||||||
|
"""New feature: resources can have a default schedule."""
|
||||||
|
data = {
|
||||||
|
"name": "Test Resource",
|
||||||
|
"type": "STAFF",
|
||||||
|
"default_schedule": {
|
||||||
|
"monday": {"start": "09:00", "end": "17:00"},
|
||||||
|
"tuesday": {"start": "09:00", "end": "17:00"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = api_client.post("/api/resources/", data, format="json")
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.data["default_schedule"]["monday"]["start"] == "09:00"
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Frontend: useResources.test.ts
|
||||||
|
it('should create resource with schedule', async () => {
|
||||||
|
const { result } = renderHook(() => useCreateResource());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({
|
||||||
|
name: 'Test Resource',
|
||||||
|
type: 'STAFF',
|
||||||
|
defaultSchedule: { monday: { start: '09:00', end: '17:00' } }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockApiClient.post).toHaveBeenCalledWith('/resources/', expect.objectContaining({
|
||||||
|
default_schedule: expect.any(Object)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run tests - they should FAIL**
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation to make tests pass**
|
||||||
|
|
||||||
|
**Step 4: Refactor if needed while keeping tests green**
|
||||||
|
|
||||||
|
### Pre-Commit Checklist
|
||||||
|
|
||||||
|
Before committing ANY code:
|
||||||
|
1. [ ] Tests written BEFORE implementation
|
||||||
|
2. [ ] All tests pass
|
||||||
|
3. [ ] Coverage meets minimum threshold (80%)
|
||||||
|
4. [ ] No skipped or disabled tests without justification
|
||||||
|
|
||||||
## CRITICAL: Backend Runs in Docker
|
## CRITICAL: Backend Runs in Docker
|
||||||
|
|
||||||
**NEVER run Django commands directly.** Always use Docker Compose:
|
**NEVER run Django commands directly.** Always use Docker Compose:
|
||||||
@@ -61,14 +224,293 @@ docker compose -f docker-compose.local.yml exec django python manage.py <command
|
|||||||
| `frontend/src/api/client.ts` | Axios API client |
|
| `frontend/src/api/client.ts` | Axios API client |
|
||||||
| `frontend/src/types.ts` | TypeScript interfaces |
|
| `frontend/src/types.ts` | TypeScript interfaces |
|
||||||
| `frontend/src/i18n/locales/en.json` | Translations |
|
| `frontend/src/i18n/locales/en.json` | Translations |
|
||||||
|
| `frontend/src/utils/dateUtils.ts` | Date formatting utilities |
|
||||||
|
|
||||||
## Key Django Apps
|
## Timezone Architecture (CRITICAL)
|
||||||
|
|
||||||
|
All date/time handling follows this architecture to ensure consistency across timezones.
|
||||||
|
|
||||||
|
### Core Principles
|
||||||
|
|
||||||
|
1. **Database**: All times stored in UTC
|
||||||
|
2. **API Communication**: Always use UTC (both directions)
|
||||||
|
3. **API Responses**: Include `business_timezone` field
|
||||||
|
4. **Frontend Display**: Convert UTC based on `business_timezone`
|
||||||
|
- If `business_timezone` is set → display in that timezone
|
||||||
|
- If `business_timezone` is null/blank → display in user's local timezone
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
FRONTEND (User in Eastern Time selects "Dec 8, 2:00 PM")
|
||||||
|
↓
|
||||||
|
Convert to UTC: "2024-12-08T19:00:00Z"
|
||||||
|
↓
|
||||||
|
Send to API (always UTC)
|
||||||
|
↓
|
||||||
|
DATABASE (stores UTC)
|
||||||
|
↓
|
||||||
|
API RESPONSE:
|
||||||
|
{
|
||||||
|
"start_time": "2024-12-08T19:00:00Z", // Always UTC
|
||||||
|
"business_timezone": "America/Denver" // IANA timezone (or null for local)
|
||||||
|
}
|
||||||
|
↓
|
||||||
|
FRONTEND CONVERTS:
|
||||||
|
- If business_timezone set: UTC → Mountain Time → "Dec 8, 12:00 PM MST"
|
||||||
|
- If business_timezone null: UTC → User local → "Dec 8, 2:00 PM EST"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Helper Functions
|
||||||
|
|
||||||
|
Located in `frontend/src/utils/dateUtils.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
toUTC,
|
||||||
|
fromUTC,
|
||||||
|
formatForDisplay,
|
||||||
|
formatDateForDisplay,
|
||||||
|
getDisplayTimezone,
|
||||||
|
} from '../utils/dateUtils';
|
||||||
|
|
||||||
|
// SENDING TO API - Always convert to UTC
|
||||||
|
const apiPayload = {
|
||||||
|
start_time: toUTC(selectedDateTime), // "2024-12-08T19:00:00Z"
|
||||||
|
};
|
||||||
|
|
||||||
|
// RECEIVING FROM API - Convert for display
|
||||||
|
const displayTime = formatForDisplay(
|
||||||
|
response.start_time, // UTC from API
|
||||||
|
response.business_timezone // "America/Denver" or null
|
||||||
|
);
|
||||||
|
// Result: "Dec 8, 2024 12:00 PM" (in business or local timezone)
|
||||||
|
|
||||||
|
// DATE-ONLY fields (time blocks)
|
||||||
|
const displayDate = formatDateForDisplay(
|
||||||
|
response.start_date,
|
||||||
|
response.business_timezone
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Response Requirements
|
||||||
|
|
||||||
|
All endpoints returning date/time data MUST include:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In serializers or views
|
||||||
|
{
|
||||||
|
"start_time": "2024-12-08T19:00:00Z",
|
||||||
|
"business_timezone": business.timezone, # "America/Denver" or None
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Serializer Mixin
|
||||||
|
|
||||||
|
Use `TimezoneSerializerMixin` from `core/mixins.py` to automatically add the timezone field:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.mixins import TimezoneSerializerMixin
|
||||||
|
|
||||||
|
class EventSerializer(TimezoneSerializerMixin, serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Event
|
||||||
|
fields = [
|
||||||
|
'id', 'start_time', 'end_time',
|
||||||
|
# ... other fields ...
|
||||||
|
'business_timezone', # Provided by mixin
|
||||||
|
]
|
||||||
|
read_only_fields = ['business_timezone']
|
||||||
|
```
|
||||||
|
|
||||||
|
The mixin automatically retrieves the timezone from the tenant context.
|
||||||
|
- Returns the IANA timezone string if set (e.g., "America/Denver")
|
||||||
|
- Returns `null` if not set (frontend uses user's local timezone)
|
||||||
|
|
||||||
|
### Common Mistakes to Avoid
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD - Uses browser local time, not UTC
|
||||||
|
date.toISOString().split('T')[0]
|
||||||
|
|
||||||
|
// BAD - Doesn't respect business timezone setting
|
||||||
|
new Date(utcString).toLocaleString()
|
||||||
|
|
||||||
|
// GOOD - Use helper functions
|
||||||
|
toUTC(date) // For API requests
|
||||||
|
formatForDisplay(utcString, businessTimezone) // For displaying
|
||||||
|
```
|
||||||
|
|
||||||
|
## Django App Organization (Domain-Based)
|
||||||
|
|
||||||
|
Apps are organized into domain packages under `smoothschedule/smoothschedule/`:
|
||||||
|
|
||||||
|
### Identity Domain
|
||||||
| App | Location | Purpose |
|
| App | Location | Purpose |
|
||||||
|-----|----------|---------|
|
|-----|----------|---------|
|
||||||
| `schedule` | `smoothschedule/smoothschedule/schedule/` | Resources, Events, Services |
|
| `core` | `identity/core/` | Tenant, Domain, PermissionGrant, middleware, mixins |
|
||||||
| `users` | `smoothschedule/smoothschedule/users/` | Authentication, User model |
|
| `users` | `identity/users/` | User model, authentication, MFA |
|
||||||
| `tenants` | `smoothschedule/smoothschedule/tenants/` | Multi-tenancy (Business model) |
|
|
||||||
|
### Scheduling Domain
|
||||||
|
| App | Location | Purpose |
|
||||||
|
|-----|----------|---------|
|
||||||
|
| `schedule` | `scheduling/schedule/` | Resources, Events, Services, Participants |
|
||||||
|
| `contracts` | `scheduling/contracts/` | Contract/e-signature system |
|
||||||
|
| `analytics` | `scheduling/analytics/` | Business analytics and reporting |
|
||||||
|
|
||||||
|
### Communication Domain
|
||||||
|
| App | Location | Purpose |
|
||||||
|
|-----|----------|---------|
|
||||||
|
| `notifications` | `communication/notifications/` | Notification system |
|
||||||
|
| `credits` | `communication/credits/` | SMS/calling credits |
|
||||||
|
| `mobile` | `communication/mobile/` | Field employee mobile app |
|
||||||
|
| `messaging` | `communication/messaging/` | Email templates and messaging |
|
||||||
|
|
||||||
|
### Commerce Domain
|
||||||
|
| App | Location | Purpose |
|
||||||
|
|-----|----------|---------|
|
||||||
|
| `payments` | `commerce/payments/` | Stripe Connect payments bridge |
|
||||||
|
| `tickets` | `commerce/tickets/` | Support ticket system |
|
||||||
|
|
||||||
|
### Platform Domain
|
||||||
|
| App | Location | Purpose |
|
||||||
|
|-----|----------|---------|
|
||||||
|
| `admin` | `platform/admin/` | Platform administration, subscriptions |
|
||||||
|
| `api` | `platform/api/` | Public API v1 for third-party integrations |
|
||||||
|
|
||||||
|
## Core Mixins & Base Classes
|
||||||
|
|
||||||
|
Located in `smoothschedule/smoothschedule/identity/core/mixins.py`. Use these to avoid code duplication.
|
||||||
|
|
||||||
|
### Permission Classes
|
||||||
|
|
||||||
|
```python
|
||||||
|
from smoothschedule.identity.core.mixins import DenyStaffWritePermission, DenyStaffAllAccessPermission, DenyStaffListPermission
|
||||||
|
|
||||||
|
class MyViewSet(ModelViewSet):
|
||||||
|
# Block write operations for staff (GET allowed)
|
||||||
|
permission_classes = [IsAuthenticated, DenyStaffWritePermission]
|
||||||
|
|
||||||
|
# Block ALL operations for staff
|
||||||
|
permission_classes = [IsAuthenticated, DenyStaffAllAccessPermission]
|
||||||
|
|
||||||
|
# Block list/create/update/delete but allow retrieve
|
||||||
|
permission_classes = [IsAuthenticated, DenyStaffListPermission]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Per-User Permission Overrides
|
||||||
|
|
||||||
|
Staff permissions can be overridden on a per-user basis using the `user.permissions` JSONField.
|
||||||
|
Permission keys are auto-derived from the view's basename or model name:
|
||||||
|
|
||||||
|
| Permission Class | Auto-derived Key | Example |
|
||||||
|
|-----------------|------------------|---------|
|
||||||
|
| `DenyStaffWritePermission` | `can_write_{basename}` | `can_write_resources` |
|
||||||
|
| `DenyStaffAllAccessPermission` | `can_access_{basename}` | `can_access_services` |
|
||||||
|
| `DenyStaffListPermission` | `can_list_{basename}` or `can_access_{basename}` | `can_list_customers` |
|
||||||
|
|
||||||
|
**Current ViewSet permission keys:**
|
||||||
|
|
||||||
|
| ViewSet | Permission Class | Override Key |
|
||||||
|
|---------|-----------------|--------------|
|
||||||
|
| `ResourceViewSet` | `DenyStaffAllAccessPermission` | `can_access_resources` |
|
||||||
|
| `ServiceViewSet` | `DenyStaffAllAccessPermission` | `can_access_services` |
|
||||||
|
| `CustomerViewSet` | `DenyStaffListPermission` | `can_list_customers` or `can_access_customers` |
|
||||||
|
| `ScheduledTaskViewSet` | `DenyStaffAllAccessPermission` | `can_access_scheduled-tasks` |
|
||||||
|
|
||||||
|
**Granting a specific staff member access:**
|
||||||
|
```bash
|
||||||
|
# Open Django shell
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py shell
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from smoothschedule.identity.users.models import User
|
||||||
|
|
||||||
|
# Find the staff member
|
||||||
|
staff = User.objects.get(email='john@example.com')
|
||||||
|
|
||||||
|
# Grant read access to resources
|
||||||
|
staff.permissions['can_access_resources'] = True
|
||||||
|
staff.save()
|
||||||
|
|
||||||
|
# Or grant list access to customers (but not full CRUD)
|
||||||
|
staff.permissions['can_list_customers'] = True
|
||||||
|
staff.save()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Custom permission keys (optional):**
|
||||||
|
```python
|
||||||
|
class ResourceViewSet(ModelViewSet):
|
||||||
|
permission_classes = [IsAuthenticated, DenyStaffAllAccessPermission]
|
||||||
|
# Override the auto-derived key
|
||||||
|
staff_access_permission_key = 'can_manage_equipment'
|
||||||
|
```
|
||||||
|
|
||||||
|
Then grant via: `staff.permissions['can_manage_equipment'] = True`
|
||||||
|
|
||||||
|
### QuerySet Mixins
|
||||||
|
|
||||||
|
```python
|
||||||
|
from smoothschedule.identity.core.mixins import TenantFilteredQuerySetMixin, UserTenantFilteredMixin
|
||||||
|
|
||||||
|
# For tenant-scoped models (automatic django-tenants filtering)
|
||||||
|
class ResourceViewSet(TenantFilteredQuerySetMixin, ModelViewSet):
|
||||||
|
queryset = Resource.objects.all()
|
||||||
|
deny_staff_queryset = True # Optional: also filter staff at queryset level
|
||||||
|
|
||||||
|
def filter_queryset_for_tenant(self, queryset):
|
||||||
|
# Override for custom filtering
|
||||||
|
return queryset.filter(is_active=True)
|
||||||
|
|
||||||
|
# For User model (shared schema, needs explicit tenant filter)
|
||||||
|
class CustomerViewSet(UserTenantFilteredMixin, ModelViewSet):
|
||||||
|
queryset = User.objects.filter(role=User.Role.CUSTOMER)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feature Permission Mixins
|
||||||
|
|
||||||
|
```python
|
||||||
|
from smoothschedule.identity.core.mixins import PluginFeatureRequiredMixin, TaskFeatureRequiredMixin
|
||||||
|
|
||||||
|
# Checks can_use_plugins feature on list/retrieve/create
|
||||||
|
class PluginViewSet(PluginFeatureRequiredMixin, ModelViewSet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Checks both can_use_plugins AND can_use_tasks
|
||||||
|
class ScheduledTaskViewSet(TaskFeatureRequiredMixin, TenantFilteredQuerySetMixin, ModelViewSet):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Base API Views (for non-ViewSet views)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from smoothschedule.identity.core.mixins import TenantAPIView, TenantRequiredAPIView
|
||||||
|
|
||||||
|
# Optional tenant - use self.get_tenant()
|
||||||
|
class MyView(TenantAPIView, APIView):
|
||||||
|
def get(self, request):
|
||||||
|
tenant = self.get_tenant() # May be None
|
||||||
|
return self.success_response({'data': 'value'})
|
||||||
|
# or: return self.error_response('Something went wrong', status_code=400)
|
||||||
|
|
||||||
|
# Required tenant - self.tenant always available
|
||||||
|
class MyTenantView(TenantRequiredAPIView, APIView):
|
||||||
|
def get(self, request):
|
||||||
|
# self.tenant is guaranteed to exist (returns 400 if missing)
|
||||||
|
return Response({'name': self.tenant.name})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helper Methods Available
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `self.get_tenant()` | Get tenant from request (may be None) |
|
||||||
|
| `self.get_tenant_or_error()` | Returns (tenant, error_response) tuple |
|
||||||
|
| `self.error_response(msg, status_code)` | Standard error response |
|
||||||
|
| `self.success_response(data, status_code)` | Standard success response |
|
||||||
|
| `self.check_feature(key, name)` | Check feature permission, returns error or None |
|
||||||
|
|
||||||
## Common Tasks
|
## Common Tasks
|
||||||
|
|
||||||
@@ -100,3 +542,66 @@ curl -s "http://lvh.me:8000/api/resources/" | jq
|
|||||||
## Git Branch
|
## Git Branch
|
||||||
Currently on: `feature/platform-superuser-ui`
|
Currently on: `feature/platform-superuser-ui`
|
||||||
Main branch: `main`
|
Main branch: `main`
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Quick Deploy
|
||||||
|
```bash
|
||||||
|
# From your local machine
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2
|
||||||
|
./deploy.sh poduck@smoothschedule.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Initial Server Setup (one-time)
|
||||||
|
```bash
|
||||||
|
# Setup server dependencies
|
||||||
|
ssh poduck@smoothschedule.com 'bash -s' < server-setup.sh
|
||||||
|
|
||||||
|
# Setup DigitalOcean Spaces
|
||||||
|
ssh poduck@smoothschedule.com
|
||||||
|
./setup-spaces.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production URLs
|
||||||
|
- **Main site:** `https://smoothschedule.com`
|
||||||
|
- **Platform dashboard:** `https://platform.smoothschedule.com`
|
||||||
|
- **Tenant subdomains:** `https://*.smoothschedule.com`
|
||||||
|
- **Flower (Celery):** `https://smoothschedule.com:5555`
|
||||||
|
|
||||||
|
### Production Management
|
||||||
|
```bash
|
||||||
|
# SSH into server
|
||||||
|
ssh poduck@smoothschedule.com
|
||||||
|
|
||||||
|
# Navigate to project
|
||||||
|
cd ~/smoothschedule
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose -f docker-compose.production.yml logs -f
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py migrate
|
||||||
|
|
||||||
|
# Create superuser
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py createsuperuser
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
docker compose -f docker-compose.production.yml restart
|
||||||
|
|
||||||
|
# View status
|
||||||
|
docker compose -f docker-compose.production.yml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
Production environment configured in:
|
||||||
|
- **Backend:** `smoothschedule/.envs/.production/.django`
|
||||||
|
- **Database:** `smoothschedule/.envs/.production/.postgres`
|
||||||
|
- **Frontend:** `frontend/.env.production`
|
||||||
|
|
||||||
|
### DigitalOcean Spaces
|
||||||
|
- **Bucket:** `smoothschedule`
|
||||||
|
- **Region:** `nyc3`
|
||||||
|
- **Endpoint:** `https://nyc3.digitaloceanspaces.com`
|
||||||
|
- **Public URL:** `https://smoothschedule.nyc3.digitaloceanspaces.com`
|
||||||
|
|
||||||
|
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed deployment guide.
|
||||||
|
|||||||
155
DATA_EXPORT_IMPLEMENTATION.md
Normal file
155
DATA_EXPORT_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Data Export API Implementation Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implemented a comprehensive data export feature for the SmoothSchedule Django backend that allows businesses to export their data in CSV and JSON formats. The feature is properly gated by subscription plan permissions.
|
||||||
|
|
||||||
|
## Implementation Date
|
||||||
|
December 2, 2025
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
### New Files Created
|
||||||
|
|
||||||
|
1. **`/home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/export_views.py`**
|
||||||
|
- Main export API implementation
|
||||||
|
- Contains `ExportViewSet` with 4 export endpoints
|
||||||
|
- Implements permission checking via `HasExportDataPermission`
|
||||||
|
- Supports both CSV and JSON formats
|
||||||
|
- ~450 lines of code
|
||||||
|
|
||||||
|
2. **`/home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/test_export.py`**
|
||||||
|
- Comprehensive test suite for export API
|
||||||
|
- Tests all endpoints, formats, filters
|
||||||
|
- Tests permission gating
|
||||||
|
- ~200 lines of test code
|
||||||
|
|
||||||
|
3. **`/home/poduck/Desktop/smoothschedule2/smoothschedule/DATA_EXPORT_API.md`**
|
||||||
|
- Complete API documentation
|
||||||
|
- Request/response examples
|
||||||
|
- Query parameter documentation
|
||||||
|
- Error handling documentation
|
||||||
|
- ~300 lines of documentation
|
||||||
|
|
||||||
|
4. **`/home/poduck/Desktop/smoothschedule2/test_export_api.py`**
|
||||||
|
- Standalone test script for manual API testing
|
||||||
|
- Can be run outside of Django test framework
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
|
||||||
|
1. **`/home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/urls.py`**
|
||||||
|
- Added import for `ExportViewSet`
|
||||||
|
- Registered export viewset with router
|
||||||
|
|
||||||
|
2. **`/home/poduck/Desktop/smoothschedule2/smoothschedule/core/models.py`**
|
||||||
|
- Added `can_export_data` BooleanField to Tenant model
|
||||||
|
- Field defaults to `False` (permission must be explicitly granted)
|
||||||
|
- Field already had migration applied (0014_tenant_can_export_data_tenant_subscription_plan.py)
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
All endpoints are accessible at the base path `/export/` (not `/api/export/` since schedule URLs are at root level).
|
||||||
|
|
||||||
|
### 1. Export Appointments
|
||||||
|
- **URL**: `GET /export/appointments/`
|
||||||
|
- **Query Params**: `format`, `start_date`, `end_date`, `status`
|
||||||
|
- **Formats**: CSV, JSON
|
||||||
|
- **Data**: Event/appointment information with customer and resource details
|
||||||
|
|
||||||
|
### 2. Export Customers
|
||||||
|
- **URL**: `GET /export/customers/`
|
||||||
|
- **Query Params**: `format`, `status`
|
||||||
|
- **Formats**: CSV, JSON
|
||||||
|
- **Data**: Customer list with contact information
|
||||||
|
|
||||||
|
### 3. Export Resources
|
||||||
|
- **URL**: `GET /export/resources/`
|
||||||
|
- **Query Params**: `format`, `is_active`
|
||||||
|
- **Formats**: CSV, JSON
|
||||||
|
- **Data**: Resource list (staff, rooms, equipment)
|
||||||
|
|
||||||
|
### 4. Export Services
|
||||||
|
- **URL**: `GET /export/services/`
|
||||||
|
- **Query Params**: `format`, `is_active`
|
||||||
|
- **Formats**: CSV, JSON
|
||||||
|
- **Data**: Service catalog with pricing and duration
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### Permission Gating
|
||||||
|
- All endpoints check `tenant.can_export_data` permission
|
||||||
|
- Returns 403 Forbidden if permission not granted
|
||||||
|
- Clear error messages guide users to upgrade their subscription
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- All endpoints require authentication (IsAuthenticated permission)
|
||||||
|
- Returns 401 Unauthorized for unauthenticated requests
|
||||||
|
|
||||||
|
### Data Isolation
|
||||||
|
- Leverages django-tenants automatic schema isolation
|
||||||
|
- Users can only export data from their own tenant
|
||||||
|
- No risk of cross-tenant data leakage
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Format Support
|
||||||
|
- **JSON**: Includes metadata (count, filters, export timestamp)
|
||||||
|
- **CSV**: Clean, spreadsheet-ready format with proper headers
|
||||||
|
- Both formats include Content-Disposition header for automatic downloads
|
||||||
|
|
||||||
|
### Filtering
|
||||||
|
- **Date Range**: Filter appointments by start_date and end_date
|
||||||
|
- **Status**: Filter by active/inactive status for various entities
|
||||||
|
- **Query Parameters**: Flexible, URL-based filtering
|
||||||
|
|
||||||
|
### File Naming
|
||||||
|
- Timestamped filenames for uniqueness
|
||||||
|
- Format: `{data_type}_{YYYYMMDD}_{HHMMSS}.{format}`
|
||||||
|
- Example: `appointments_20241202_103000.csv`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run unit tests with:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py test schedule.test_export
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
### Enable Export for a Tenant
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In Django shell or admin
|
||||||
|
from core.models import Tenant
|
||||||
|
|
||||||
|
tenant = Tenant.objects.get(schema_name='your_tenant')
|
||||||
|
tenant.can_export_data = True
|
||||||
|
tenant.save()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example API Calls
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# JSON export
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
"http://lvh.me:8000/export/appointments/?format=json"
|
||||||
|
|
||||||
|
# CSV export with date range
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
"http://lvh.me:8000/export/appointments/?format=csv&start_date=2024-01-01T00:00:00Z&end_date=2024-12-31T23:59:59Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Checklist
|
||||||
|
|
||||||
|
- [x] Permission gating implemented
|
||||||
|
- [x] Authentication required
|
||||||
|
- [x] Unit tests written
|
||||||
|
- [x] Documentation created
|
||||||
|
- [x] Database migration applied
|
||||||
|
- [ ] Rate limiting configured
|
||||||
|
- [ ] Frontend integration completed
|
||||||
|
- [ ] Load testing performed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Implementation completed successfully!** ✓
|
||||||
449
DEPLOYMENT.md
Normal file
449
DEPLOYMENT.md
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
# SmoothSchedule Production Deployment Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Server Requirements
|
||||||
|
- Ubuntu/Debian Linux server
|
||||||
|
- Minimum 2GB RAM, 20GB disk space
|
||||||
|
- Docker and Docker Compose installed
|
||||||
|
- Domain name pointed to server IP: `smoothschedule.com`
|
||||||
|
- DNS configured with wildcard subdomain: `*.smoothschedule.com`
|
||||||
|
|
||||||
|
### Required Accounts/Services
|
||||||
|
- [x] DigitalOcean Spaces (already configured)
|
||||||
|
- Access Key: DO801P4R8QXYMY4CE8WZ
|
||||||
|
- Bucket: smoothschedule
|
||||||
|
- Region: nyc3
|
||||||
|
- [ ] Email service (optional - Mailgun or SMTP)
|
||||||
|
- [ ] Sentry (optional - error tracking)
|
||||||
|
|
||||||
|
## Pre-Deployment Checklist
|
||||||
|
|
||||||
|
### 1. DigitalOcean Spaces Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create the bucket (if not already created)
|
||||||
|
aws --profile do-tor1 s3 mb s3://smoothschedule
|
||||||
|
|
||||||
|
# Set bucket to public-read for static/media files
|
||||||
|
aws --profile do-tor1 s3api put-bucket-acl \
|
||||||
|
--bucket smoothschedule \
|
||||||
|
--acl public-read
|
||||||
|
|
||||||
|
# Configure CORS (for frontend uploads)
|
||||||
|
cat > cors.json <<EOF
|
||||||
|
{
|
||||||
|
"CORSRules": [
|
||||||
|
{
|
||||||
|
"AllowedOrigins": ["https://smoothschedule.com", "https://*.smoothschedule.com"],
|
||||||
|
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
|
||||||
|
"AllowedHeaders": ["*"],
|
||||||
|
"MaxAgeSeconds": 3000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
aws --profile do-tor1 s3api put-bucket-cors \
|
||||||
|
--bucket smoothschedule \
|
||||||
|
--cors-configuration file://cors.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. DNS Configuration
|
||||||
|
|
||||||
|
Configure these DNS records at your domain registrar:
|
||||||
|
|
||||||
|
```
|
||||||
|
Type Name Value TTL
|
||||||
|
A smoothschedule.com YOUR_SERVER_IP 300
|
||||||
|
A *.smoothschedule.com YOUR_SERVER_IP 300
|
||||||
|
CNAME www smoothschedule.com 300
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Environment Variables Review
|
||||||
|
|
||||||
|
**Backend** (`.envs/.production/.django`):
|
||||||
|
- [x] DJANGO_SECRET_KEY - Set
|
||||||
|
- [x] DJANGO_ALLOWED_HOSTS - Set to `.smoothschedule.com`
|
||||||
|
- [x] DJANGO_AWS_ACCESS_KEY_ID - Set
|
||||||
|
- [x] DJANGO_AWS_SECRET_ACCESS_KEY - Set
|
||||||
|
- [x] DJANGO_AWS_STORAGE_BUCKET_NAME - Set to `smoothschedule`
|
||||||
|
- [x] DJANGO_AWS_S3_ENDPOINT_URL - Set to `https://nyc3.digitaloceanspaces.com`
|
||||||
|
- [x] DJANGO_AWS_S3_REGION_NAME - Set to `nyc3`
|
||||||
|
- [ ] MAILGUN_API_KEY - Optional (for email)
|
||||||
|
- [ ] MAILGUN_DOMAIN - Optional (for email)
|
||||||
|
- [ ] SENTRY_DSN - Optional (for error tracking)
|
||||||
|
|
||||||
|
**Frontend** (`.env.production`):
|
||||||
|
- [x] VITE_API_URL - Set to `https://smoothschedule.com/api`
|
||||||
|
|
||||||
|
## Deployment Steps
|
||||||
|
|
||||||
|
### Step 1: Server Preparation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH into production server
|
||||||
|
ssh poduck@smoothschedule.com
|
||||||
|
|
||||||
|
# Install Docker (if not already installed)
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sudo sh get-docker.sh
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
|
||||||
|
# Install Docker Compose
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
# Logout and login again for group changes to take effect
|
||||||
|
exit
|
||||||
|
ssh poduck@smoothschedule.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Deploy Backend (Django)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create deployment directory
|
||||||
|
mkdir -p ~/smoothschedule
|
||||||
|
cd ~/smoothschedule
|
||||||
|
|
||||||
|
# Clone the repository (or upload files via rsync/git)
|
||||||
|
# Option A: Clone from Git
|
||||||
|
git clone <your-repo-url> .
|
||||||
|
git checkout main
|
||||||
|
|
||||||
|
# Option B: Copy from local machine
|
||||||
|
# From your local machine:
|
||||||
|
# rsync -avz --exclude 'node_modules' --exclude '.venv' --exclude '__pycache__' \
|
||||||
|
# /home/poduck/Desktop/smoothschedule2/ poduck@smoothschedule.com:~/smoothschedule/
|
||||||
|
|
||||||
|
# Navigate to backend
|
||||||
|
cd smoothschedule
|
||||||
|
|
||||||
|
# Build and start containers
|
||||||
|
docker compose -f docker-compose.production.yml build
|
||||||
|
docker compose -f docker-compose.production.yml up -d
|
||||||
|
|
||||||
|
# Wait for containers to start
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker compose -f docker-compose.production.yml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Database Initialization
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run migrations
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py migrate
|
||||||
|
|
||||||
|
# Create public schema (for multi-tenancy)
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py migrate_schemas --shared
|
||||||
|
|
||||||
|
# Create superuser
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py createsuperuser
|
||||||
|
|
||||||
|
# Collect static files (uploads to DigitalOcean Spaces)
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py collectstatic --noinput
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Create Initial Tenant
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Access Django shell
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py shell
|
||||||
|
|
||||||
|
# In the shell, create your first business tenant:
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.models import Business
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
# Create a business
|
||||||
|
business = Business.objects.create(
|
||||||
|
name="Demo Business",
|
||||||
|
subdomain="demo",
|
||||||
|
schema_name="demo",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify it was created
|
||||||
|
print(f"Created business: {business.name} at {business.subdomain}.smoothschedule.com")
|
||||||
|
|
||||||
|
# Create a business owner
|
||||||
|
owner = User.objects.create_user(
|
||||||
|
username="demo_owner",
|
||||||
|
email="owner@demo.com",
|
||||||
|
password="your_password_here",
|
||||||
|
role="owner",
|
||||||
|
business_subdomain="demo"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Created owner: {owner.username}")
|
||||||
|
exit()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Deploy Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On your local machine
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2/frontend
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Upload build files to server
|
||||||
|
rsync -avz dist/ poduck@smoothschedule.com:~/smoothschedule-frontend/
|
||||||
|
|
||||||
|
# On the server, set up nginx or serve via backend
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option A: Serve via Django (simpler)**
|
||||||
|
|
||||||
|
The Django `collectstatic` command already handles static files. For serving the frontend:
|
||||||
|
|
||||||
|
1. Copy frontend build to Django static folder
|
||||||
|
2. Django will serve it via Traefik
|
||||||
|
|
||||||
|
**Option B: Separate Nginx (recommended for production)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install nginx
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y nginx
|
||||||
|
|
||||||
|
# Create nginx config
|
||||||
|
sudo nano /etc/nginx/sites-available/smoothschedule
|
||||||
|
```
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name smoothschedule.com *.smoothschedule.com;
|
||||||
|
|
||||||
|
# Frontend (React)
|
||||||
|
location / {
|
||||||
|
root /home/poduck/smoothschedule-frontend;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backend API (proxy to Traefik)
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://localhost:80;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /admin {
|
||||||
|
proxy_pass http://localhost:80;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable site
|
||||||
|
sudo ln -s /etc/nginx/sites-available/smoothschedule /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: SSL/HTTPS Setup
|
||||||
|
|
||||||
|
Traefik is configured to automatically obtain Let's Encrypt SSL certificates. Ensure:
|
||||||
|
|
||||||
|
1. DNS is pointed to your server
|
||||||
|
2. Ports 80 and 443 are accessible
|
||||||
|
3. Wait for Traefik to obtain certificates (check logs)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Monitor Traefik logs
|
||||||
|
docker compose -f docker-compose.production.yml logs -f traefik
|
||||||
|
|
||||||
|
# You should see:
|
||||||
|
# "Certificate obtained for domain smoothschedule.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Verify Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check all containers are running
|
||||||
|
docker compose -f docker-compose.production.yml ps
|
||||||
|
|
||||||
|
# Should show:
|
||||||
|
# - django (running)
|
||||||
|
# - postgres (running)
|
||||||
|
# - redis (running)
|
||||||
|
# - traefik (running)
|
||||||
|
# - celeryworker (running)
|
||||||
|
# - celerybeat (running)
|
||||||
|
# - flower (running)
|
||||||
|
|
||||||
|
# Test API endpoint
|
||||||
|
curl https://smoothschedule.com/api/
|
||||||
|
|
||||||
|
# Test admin
|
||||||
|
curl https://smoothschedule.com/admin/
|
||||||
|
|
||||||
|
# Access in browser:
|
||||||
|
# https://smoothschedule.com - Main site
|
||||||
|
# https://platform.smoothschedule.com - Platform dashboard
|
||||||
|
# https://demo.smoothschedule.com - Demo business
|
||||||
|
# https://smoothschedule.com:5555 - Flower (Celery monitoring)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Post-Deployment
|
||||||
|
|
||||||
|
### 1. Monitoring
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View logs
|
||||||
|
docker compose -f docker-compose.production.yml logs -f
|
||||||
|
|
||||||
|
# View specific service logs
|
||||||
|
docker compose -f docker-compose.production.yml logs -f django
|
||||||
|
docker compose -f docker-compose.production.yml logs -f postgres
|
||||||
|
|
||||||
|
# Monitor Celery tasks via Flower
|
||||||
|
# Access: https://smoothschedule.com:5555
|
||||||
|
# Login with credentials from .envs/.production/.django
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Backups
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database backup
|
||||||
|
docker compose -f docker-compose.production.yml exec postgres backup
|
||||||
|
|
||||||
|
# List backups
|
||||||
|
docker compose -f docker-compose.production.yml exec postgres backups
|
||||||
|
|
||||||
|
# Restore from backup
|
||||||
|
docker compose -f docker-compose.production.yml exec postgres restore backup_filename.sql.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Updates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull latest code
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Rebuild and restart
|
||||||
|
docker compose -f docker-compose.production.yml build
|
||||||
|
docker compose -f docker-compose.production.yml up -d
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py migrate
|
||||||
|
|
||||||
|
# Collect static files
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py collectstatic --noinput
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### SSL Certificate Issues
|
||||||
|
```bash
|
||||||
|
# Check Traefik logs
|
||||||
|
docker compose -f docker-compose.production.yml logs traefik
|
||||||
|
|
||||||
|
# Verify DNS is pointing to server
|
||||||
|
dig smoothschedule.com +short
|
||||||
|
|
||||||
|
# Ensure ports are open
|
||||||
|
sudo ufw allow 80
|
||||||
|
sudo ufw allow 443
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Connection Issues
|
||||||
|
```bash
|
||||||
|
# Check PostgreSQL is running
|
||||||
|
docker compose -f docker-compose.production.yml ps postgres
|
||||||
|
|
||||||
|
# Check database logs
|
||||||
|
docker compose -f docker-compose.production.yml logs postgres
|
||||||
|
|
||||||
|
# Verify connection
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py dbshell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static Files Not Loading
|
||||||
|
```bash
|
||||||
|
# Verify DigitalOcean Spaces credentials
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py shell
|
||||||
|
>>> from django.conf import settings
|
||||||
|
>>> print(settings.AWS_ACCESS_KEY_ID)
|
||||||
|
>>> print(settings.AWS_STORAGE_BUCKET_NAME)
|
||||||
|
|
||||||
|
# Re-collect static files
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
# Check Spaces bucket
|
||||||
|
aws --profile do-tor1 s3 ls s3://smoothschedule/static/
|
||||||
|
aws --profile do-tor1 s3 ls s3://smoothschedule/media/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Celery Not Running Tasks
|
||||||
|
```bash
|
||||||
|
# Check Celery worker logs
|
||||||
|
docker compose -f docker-compose.production.yml logs celeryworker
|
||||||
|
|
||||||
|
# Access Flower dashboard
|
||||||
|
# https://smoothschedule.com:5555
|
||||||
|
|
||||||
|
# Restart Celery
|
||||||
|
docker compose -f docker-compose.production.yml restart celeryworker celerybeat
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
- [x] SSL/HTTPS enabled via Let's Encrypt
|
||||||
|
- [x] DJANGO_SECRET_KEY set to random value
|
||||||
|
- [x] Database password set to random value
|
||||||
|
- [x] Flower dashboard password protected
|
||||||
|
- [ ] Firewall configured (UFW or iptables)
|
||||||
|
- [ ] SSH key-based authentication enabled
|
||||||
|
- [ ] Fail2ban installed for brute-force protection
|
||||||
|
- [ ] Regular backups configured
|
||||||
|
- [ ] Sentry error monitoring (optional)
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
1. **Enable CDN for DigitalOcean Spaces**
|
||||||
|
- In Spaces settings, enable CDN
|
||||||
|
- Update `DJANGO_AWS_S3_CUSTOM_DOMAIN=smoothschedule.nyc3.cdn.digitaloceanspaces.com`
|
||||||
|
|
||||||
|
2. **Scale Gunicorn Workers**
|
||||||
|
- Adjust `WEB_CONCURRENCY` in `.envs/.production/.django`
|
||||||
|
- Formula: (2 x CPU cores) + 1
|
||||||
|
|
||||||
|
3. **Add Redis Persistence**
|
||||||
|
- Update docker-compose.production.yml redis config
|
||||||
|
- Enable AOF persistence
|
||||||
|
|
||||||
|
4. **Database Connection Pooling**
|
||||||
|
- Already configured via `CONN_MAX_AGE=60`
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Weekly
|
||||||
|
- Review error logs
|
||||||
|
- Check disk space: `df -h`
|
||||||
|
- Monitor Flower dashboard for failed tasks
|
||||||
|
|
||||||
|
### Monthly
|
||||||
|
- Update Docker images: `docker compose pull`
|
||||||
|
- Update dependencies: `uv sync`
|
||||||
|
- Review backups
|
||||||
|
|
||||||
|
### As Needed
|
||||||
|
- Scale resources (CPU/RAM)
|
||||||
|
- Add more Celery workers
|
||||||
|
- Optimize database queries
|
||||||
56
ESTIMATE.md
Normal file
56
ESTIMATE.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
### **Project Estimate: SmoothSchedule Platform Development**
|
||||||
|
|
||||||
|
**Date:** December 16, 2025
|
||||||
|
**Prepared For:** Internal & External Stakeholders
|
||||||
|
**Prepared By:** Gemini AI Software Engineering Agent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **1. Executive Summary**
|
||||||
|
|
||||||
|
This document provides a high-level cost and timeline estimate for the from-scratch development of the SmoothSchedule platform. Our analysis of the existing codebase reveals that SmoothSchedule is not merely a scheduling application, but a sophisticated, multi-tenant Software-as-a-Service (SaaS) platform with enterprise-grade features for e-commerce, extensive customization, and developer-level extensibility.
|
||||||
|
|
||||||
|
The architecture includes several high-complexity components, most notably a **sandboxed custom scripting engine** for user-defined automations and a **full data-isolation sandbox mode** for testing. These features place the project in a category of complexity comparable to building a platform-as-a-service (PaaS).
|
||||||
|
|
||||||
|
* **Total Estimated Project Cost:** **$3,500,000 - $5,500,000 USD**
|
||||||
|
* **Estimated Timeline:** **18 - 24 months**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **2. Project Scope & Complexity Analysis**
|
||||||
|
|
||||||
|
This estimate is based on the implementation of the following key features and architectural pillars identified in the codebase:
|
||||||
|
|
||||||
|
* **Multi-Tenant SaaS Architecture:** The system is designed to serve multiple businesses (tenants) from a single infrastructure, with complete data separation.
|
||||||
|
* **Custom Python Scripting Engine:** The platform's most complex feature is its ability to safely execute custom, user-written Python code in a sandboxed environment. This allows for limitless business automation, similar to Zapier or Salesforce Apex, and requires significant investment in security and resource management.
|
||||||
|
* **Full Data-Isolation Sandbox Mode:** A complete "Test Mode" for each tenant, which uses a separate, isolated database schema. This allows users to safely test workflows and automations without affecting their live business data—a feature typical of mature, enterprise-grade platforms.
|
||||||
|
* **Extensible Automation & Plugin Framework:** A comprehensive system for creating, installing, and managing "plugins" (automations). These can be triggered by schedules (e.g., cron jobs) or application events (e.g., when an appointment is completed), and are managed via a built-in marketplace.
|
||||||
|
* **Visual Page Builder:** A drag-and-drop interface (identified as using `@measured/puck`) that allows tenants to build their own public-facing websites and booking pages.
|
||||||
|
* **Advanced E-commerce Platform:** Integration with Stripe Connect, enabling tenants to bill their own customers, not just for the platform to bill its tenants. This requires complex logic for managed accounts, payment flows, and fee processing.
|
||||||
|
* **Real-time Capabilities & Asynchronous Tasks:** The platform uses WebSockets (`django-channels`) for live updates and a Celery-based queue for handling background jobs like sending emails, generating reports, and running automations.
|
||||||
|
* **Mobile Application:** A companion field application for iOS and Android that integrates with the full feature set of the backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **3. Cost Estimate Breakdown**
|
||||||
|
|
||||||
|
This estimate assumes a US-based software development agency model, including project management, design, development, and quality assurance.
|
||||||
|
|
||||||
|
| Category | Description | Estimated Cost Range |
|
||||||
|
| :---------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------ |
|
||||||
|
| **Backend Development** | (Python/Django) Includes multi-tenancy, database architecture, REST APIs, and the highly complex scripting engine & sandbox mode. | $1,500,000 - $2,200,000 |
|
||||||
|
| **Frontend Development** | (React/TypeScript) Includes the primary web application, visual page builder, custom dashboards, and UIs for the automation/scripting engine. | $700,000 - $1,100,000 |
|
||||||
|
| **Mobile App Development** | (iOS/Android) Native or cross-platform development of the companion field application. | $350,000 - $550,000 |
|
||||||
|
| **UI/UX Design** | Wireframing, prototyping, and high-fidelity design for the web and mobile applications, ensuring a polished and intuitive user experience. | $250,000 - $400,000 |
|
||||||
|
| **Project Management & QA** | Management oversight, sprint planning, manual and automated testing, and release coordination across all development tracks. | $550,000 - $900,000 |
|
||||||
|
| **Third-Party Security Audit** | Essential for a platform that executes custom code. Includes penetration testing and code review by an external security firm. | $100,000 - $350,000 |
|
||||||
|
| **Total Estimated Cost** | | **$3,500,000 - $5,500,000** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **4. Assumptions & Disclaimer**
|
||||||
|
|
||||||
|
* This estimate is based on a feature set inferred from a comprehensive analysis of the provided codebase.
|
||||||
|
* The costs are calculated using a blended agency rate typical for senior-level engineering talent in the United States. Rates and timelines may vary based on team location and composition.
|
||||||
|
* This document is a high-level estimate intended for budgetary and planning purposes only. It is **not** a fixed-price quote. A formal proposal would require a detailed discovery and specification phase.
|
||||||
|
* This estimate covers initial development and deployment. It does **not** include ongoing operational costs such as hosting, third-party service fees (e.g., Stripe, Twilio), marketing, or post-launch maintenance.
|
||||||
286
IMPLEMENTATION_COMPLETE.md
Normal file
286
IMPLEMENTATION_COMPLETE.md
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# Advanced Analytics Implementation - Complete
|
||||||
|
|
||||||
|
## Status: ✅ COMPLETE
|
||||||
|
|
||||||
|
All files have been created and configured successfully. The advanced analytics feature is fully implemented with permission-based access control.
|
||||||
|
|
||||||
|
## What Was Implemented
|
||||||
|
|
||||||
|
### New Analytics App
|
||||||
|
- **Location:** `/smoothschedule/analytics/`
|
||||||
|
- **Endpoints:** 3 analytics endpoints with permission gating
|
||||||
|
- **Permissions:** All endpoints gated by `advanced_analytics` permission
|
||||||
|
- **Tests:** 10 comprehensive test cases
|
||||||
|
|
||||||
|
### 3 Analytics Endpoints
|
||||||
|
|
||||||
|
1. **Dashboard** (`GET /api/analytics/analytics/dashboard/`)
|
||||||
|
- Summary statistics
|
||||||
|
- Total appointments, resources, services
|
||||||
|
- Peak times and trends
|
||||||
|
|
||||||
|
2. **Appointments** (`GET /api/analytics/analytics/appointments/`)
|
||||||
|
- Detailed appointment analytics
|
||||||
|
- Filtering by status, service, resource, date range
|
||||||
|
- Status breakdown and trend analysis
|
||||||
|
|
||||||
|
3. **Revenue** (`GET /api/analytics/analytics/revenue/`)
|
||||||
|
- Payment analytics
|
||||||
|
- Requires both `advanced_analytics` AND `can_accept_payments`
|
||||||
|
- Revenue by service and daily breakdown
|
||||||
|
|
||||||
|
## Permission Gating
|
||||||
|
|
||||||
|
All endpoints use:
|
||||||
|
- **IsAuthenticated** - Requires login
|
||||||
|
- **HasFeaturePermission('advanced_analytics')** - Requires subscription plan permission
|
||||||
|
|
||||||
|
Permission chain:
|
||||||
|
```
|
||||||
|
Request → IsAuthenticated (401) → HasFeaturePermission (403) → View
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Created (11 total)
|
||||||
|
|
||||||
|
### Core App Files
|
||||||
|
```
|
||||||
|
analytics/
|
||||||
|
├── __init__.py
|
||||||
|
├── admin.py
|
||||||
|
├── apps.py
|
||||||
|
├── migrations/__init__.py
|
||||||
|
├── views.py (350+ lines, 3 endpoints)
|
||||||
|
├── serializers.py (80+ lines)
|
||||||
|
├── urls.py
|
||||||
|
└── tests.py (260+ lines, 10 test cases)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
```
|
||||||
|
analytics/
|
||||||
|
├── README.md (Full API documentation)
|
||||||
|
└── IMPLEMENTATION_GUIDE.md (Developer guide)
|
||||||
|
|
||||||
|
Project Root:
|
||||||
|
├── ANALYTICS_CHANGES.md (Change summary)
|
||||||
|
└── analytics/ANALYTICS_IMPLEMENTATION_SUMMARY.md (Complete overview)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Modified (3 total)
|
||||||
|
|
||||||
|
### 1. `/smoothschedule/core/permissions.py`
|
||||||
|
- Added to FEATURE_NAMES dictionary:
|
||||||
|
- 'advanced_analytics': 'Advanced Analytics'
|
||||||
|
- 'advanced_reporting': 'Advanced Reporting'
|
||||||
|
|
||||||
|
### 2. `/smoothschedule/config/urls.py`
|
||||||
|
- Added: `path("", include("analytics.urls"))`
|
||||||
|
|
||||||
|
### 3. `/smoothschedule/config/settings/base.py`
|
||||||
|
- Added "analytics" to LOCAL_APPS
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### Enable Analytics for a Plan
|
||||||
|
|
||||||
|
**Option 1: Django Admin**
|
||||||
|
```
|
||||||
|
1. Go to /admin/platform_admin/subscriptionplan/
|
||||||
|
2. Edit a plan
|
||||||
|
3. Add to Permissions JSON: "advanced_analytics": true
|
||||||
|
4. Save
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Django Shell**
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py shell
|
||||||
|
|
||||||
|
from platform_admin.models import SubscriptionPlan
|
||||||
|
plan = SubscriptionPlan.objects.get(name='Professional')
|
||||||
|
perms = plan.permissions or {}
|
||||||
|
perms['advanced_analytics'] = True
|
||||||
|
plan.permissions = perms
|
||||||
|
plan.save()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test the Endpoints
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get auth token
|
||||||
|
TOKEN=$(curl -X POST http://lvh.me:8000/auth-token/ \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"test@example.com","password":"password"}' | jq -r '.token')
|
||||||
|
|
||||||
|
# Get dashboard analytics
|
||||||
|
curl -H "Authorization: Token $TOKEN" \
|
||||||
|
http://lvh.me:8000/api/analytics/analytics/dashboard/ | jq
|
||||||
|
|
||||||
|
# Get appointment analytics
|
||||||
|
curl -H "Authorization: Token $TOKEN" \
|
||||||
|
"http://lvh.me:8000/api/analytics/analytics/appointments/?days=7" | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All tests
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest analytics/tests.py -v
|
||||||
|
|
||||||
|
# Specific test
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest analytics/tests.py::TestAnalyticsPermissions::test_analytics_denied_without_permission -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
- [x] Analytics app created with proper structure
|
||||||
|
- [x] Three endpoints implemented (dashboard, appointments, revenue)
|
||||||
|
- [x] Permission gating with HasFeaturePermission
|
||||||
|
- [x] Advanced analytics permission added to FEATURE_NAMES
|
||||||
|
- [x] URL routing configured
|
||||||
|
- [x] App registered in INSTALLED_APPS
|
||||||
|
- [x] Serializers created for response validation
|
||||||
|
- [x] Comprehensive test suite (10 tests)
|
||||||
|
- [x] Full API documentation
|
||||||
|
- [x] Implementation guide for developers
|
||||||
|
- [x] All files in place and verified
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
✓ **Permission-Based Access Control**
|
||||||
|
- Uses standard HasFeaturePermission pattern
|
||||||
|
- Supports both direct fields and plan JSON
|
||||||
|
- User-friendly error messages
|
||||||
|
|
||||||
|
✓ **Three Functional Endpoints**
|
||||||
|
- Dashboard: Summary statistics
|
||||||
|
- Appointments: Detailed analytics with filters
|
||||||
|
- Revenue: Payment analytics (dual-permission)
|
||||||
|
|
||||||
|
✓ **Comprehensive Testing**
|
||||||
|
- 10 test cases covering all scenarios
|
||||||
|
- Permission checks verified
|
||||||
|
- Data calculations validated
|
||||||
|
|
||||||
|
✓ **Complete Documentation**
|
||||||
|
- API documentation with examples
|
||||||
|
- Implementation guide
|
||||||
|
- Code comments and docstrings
|
||||||
|
- Test examples
|
||||||
|
|
||||||
|
✓ **No Database Migrations**
|
||||||
|
- Analytics app has no models
|
||||||
|
- Uses existing models (Event, Service, Resource)
|
||||||
|
- Calculated on-demand
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Code Review** - Review the implementation
|
||||||
|
2. **Testing** - Run test suite: `pytest analytics/tests.py -v`
|
||||||
|
3. **Enable Plans** - Add permission to subscription plans
|
||||||
|
4. **Deploy** - Push to production
|
||||||
|
5. **Monitor** - Watch for usage and issues
|
||||||
|
|
||||||
|
## Documentation Files
|
||||||
|
|
||||||
|
- **README.md** - Complete API documentation with usage examples
|
||||||
|
- **IMPLEMENTATION_GUIDE.md** - Developer guide with setup instructions
|
||||||
|
- **ANALYTICS_CHANGES.md** - Summary of all changes made
|
||||||
|
- **ANALYTICS_IMPLEMENTATION_SUMMARY.md** - Detailed implementation overview
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/poduck/Desktop/smoothschedule2/
|
||||||
|
├── smoothschedule/
|
||||||
|
│ ├── analytics/ ← NEW APP
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── admin.py
|
||||||
|
│ │ ├── apps.py
|
||||||
|
│ │ ├── views.py ← 350+ lines
|
||||||
|
│ │ ├── serializers.py
|
||||||
|
│ │ ├── urls.py
|
||||||
|
│ │ ├── tests.py ← 10 test cases
|
||||||
|
│ │ ├── migrations/
|
||||||
|
│ │ ├── README.md ← Full API docs
|
||||||
|
│ │ └── IMPLEMENTATION_GUIDE.md ← Developer guide
|
||||||
|
│ ├── core/
|
||||||
|
│ │ └── permissions.py ← MODIFIED
|
||||||
|
│ ├── config/
|
||||||
|
│ │ ├── urls.py ← MODIFIED
|
||||||
|
│ │ └── settings/base.py ← MODIFIED
|
||||||
|
│ └── [other apps...]
|
||||||
|
│
|
||||||
|
├── ANALYTICS_CHANGES.md ← Change summary
|
||||||
|
└── IMPLEMENTATION_COMPLETE.md ← This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Statistics
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| New Files Created | 11 |
|
||||||
|
| Files Modified | 3 |
|
||||||
|
| New Lines of Code | 900+ |
|
||||||
|
| API Endpoints | 3 |
|
||||||
|
| Test Cases | 10 |
|
||||||
|
| Documentation Pages | 4 |
|
||||||
|
| Query Parameters Supported | 6 |
|
||||||
|
|
||||||
|
## Response Examples
|
||||||
|
|
||||||
|
### Dashboard (200 OK)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total_appointments_this_month": 42,
|
||||||
|
"total_appointments_all_time": 1250,
|
||||||
|
"active_resources_count": 5,
|
||||||
|
"active_services_count": 3,
|
||||||
|
"upcoming_appointments_count": 8,
|
||||||
|
"average_appointment_duration_minutes": 45.5,
|
||||||
|
"peak_booking_day": "Friday",
|
||||||
|
"peak_booking_hour": 14,
|
||||||
|
"period": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Denied (403 Forbidden)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Your current plan does not include Advanced Analytics. Please upgrade your subscription to access this feature."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unauthorized (401 Unauthorized)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Authentication credentials were not provided."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Quality
|
||||||
|
|
||||||
|
- ✓ Follows DRF best practices
|
||||||
|
- ✓ Uses existing permission patterns (HasFeaturePermission)
|
||||||
|
- ✓ Comprehensive error handling
|
||||||
|
- ✓ Full test coverage
|
||||||
|
- ✓ Clear documentation
|
||||||
|
- ✓ Code comments
|
||||||
|
- ✓ Consistent with project style
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For questions or issues:
|
||||||
|
|
||||||
|
1. **API Usage** → See `analytics/README.md`
|
||||||
|
2. **Setup & Debugging** → See `analytics/IMPLEMENTATION_GUIDE.md`
|
||||||
|
3. **Permission Logic** → See `core/permissions.py`
|
||||||
|
4. **Test Examples** → See `analytics/tests.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status: Ready for Production** ✅
|
||||||
|
|
||||||
|
All implementation, testing, and documentation are complete.
|
||||||
|
The advanced analytics feature is fully functional with permission-based access control.
|
||||||
|
|
||||||
|
Last Updated: December 2, 2025
|
||||||
383
PLAN_APP_REORGANIZATION.md
Normal file
383
PLAN_APP_REORGANIZATION.md
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
# Django App Reorganization Plan - Option C (Domain-Based)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Reorganize Django apps from their current scattered locations into a clean domain-based structure within `smoothschedule/smoothschedule/`.
|
||||||
|
|
||||||
|
**Branch:** `refactor/organize-django-apps`
|
||||||
|
**Risk Level:** Medium-High (migration history must be preserved)
|
||||||
|
**Estimated Parallel Agents:** 6-8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current State Analysis
|
||||||
|
|
||||||
|
### Current App Locations (Inconsistent)
|
||||||
|
|
||||||
|
| App | Current Location | Registered As |
|
||||||
|
|-----|-----------------|---------------|
|
||||||
|
| core | `smoothschedule/core/` | `"core"` |
|
||||||
|
| schedule | `smoothschedule/schedule/` | `"schedule"` |
|
||||||
|
| payments | `smoothschedule/payments/` | `"payments"` |
|
||||||
|
| platform_admin | `smoothschedule/platform_admin/` | `"platform_admin.apps.PlatformAdminConfig"` |
|
||||||
|
| analytics | `smoothschedule/analytics/` | `"analytics"` |
|
||||||
|
| notifications | `smoothschedule/notifications/` | `"notifications"` |
|
||||||
|
| tickets | `smoothschedule/tickets/` | `"tickets"` |
|
||||||
|
| contracts | `smoothschedule/contracts/` | **NOT REGISTERED** |
|
||||||
|
| communication | `smoothschedule/communication/` | **NOT REGISTERED** |
|
||||||
|
| users | `smoothschedule/smoothschedule/users/` | `"smoothschedule.users"` |
|
||||||
|
| comms_credits | `smoothschedule/smoothschedule/comms_credits/` | `"smoothschedule.comms_credits"` |
|
||||||
|
| field_mobile | `smoothschedule/smoothschedule/field_mobile/` | `"smoothschedule.field_mobile"` |
|
||||||
|
| public_api | `smoothschedule/smoothschedule/public_api/` | `"smoothschedule.public_api"` |
|
||||||
|
|
||||||
|
### Migration Counts by App
|
||||||
|
|
||||||
|
| App | Migrations | Complexity |
|
||||||
|
|-----|------------|------------|
|
||||||
|
| core | 22 | High (Tenant model) |
|
||||||
|
| schedule | 30 | High (main business logic) |
|
||||||
|
| payments | 1 | Low |
|
||||||
|
| platform_admin | 12 | Medium |
|
||||||
|
| users | 10 | Medium |
|
||||||
|
| tickets | 13 | Medium |
|
||||||
|
| contracts | 1 | Low |
|
||||||
|
| notifications | 1 | Low |
|
||||||
|
| comms_credits | 2 | Low |
|
||||||
|
| field_mobile | 1 | Low |
|
||||||
|
| public_api | 3 | Low |
|
||||||
|
| analytics | 0 | None |
|
||||||
|
| communication | 1 | Low |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Target Structure (Option C - Domain-Based)
|
||||||
|
|
||||||
|
```
|
||||||
|
smoothschedule/smoothschedule/
|
||||||
|
├── __init__.py
|
||||||
|
│
|
||||||
|
├── identity/ # User & Tenant Management
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── core/ # Multi-tenancy, permissions, OAuth
|
||||||
|
│ │ └── (moved from smoothschedule/core/)
|
||||||
|
│ └── users/ # User model, auth, invitations
|
||||||
|
│ └── (keep at current location, just move parent)
|
||||||
|
│
|
||||||
|
├── scheduling/ # Core Business Logic
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── schedule/ # Resources, Events, Services, Plugins
|
||||||
|
│ │ └── (moved from smoothschedule/schedule/)
|
||||||
|
│ ├── contracts/ # E-signatures, legal documents
|
||||||
|
│ │ └── (moved from smoothschedule/contracts/)
|
||||||
|
│ └── analytics/ # Reporting, dashboards
|
||||||
|
│ └── (moved from smoothschedule/analytics/)
|
||||||
|
│
|
||||||
|
├── communication/ # Messaging & Notifications
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── notifications/ # In-app notifications
|
||||||
|
│ │ └── (moved from smoothschedule/notifications/)
|
||||||
|
│ ├── credits/ # SMS/voice credits (renamed from comms_credits)
|
||||||
|
│ │ └── (moved from smoothschedule/smoothschedule/comms_credits/)
|
||||||
|
│ ├── mobile/ # Field employee app (renamed from field_mobile)
|
||||||
|
│ │ └── (moved from smoothschedule/smoothschedule/field_mobile/)
|
||||||
|
│ └── messaging/ # Twilio conversations (renamed from communication)
|
||||||
|
│ └── (moved from smoothschedule/communication/)
|
||||||
|
│
|
||||||
|
├── commerce/ # Payments & Support
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── payments/ # Stripe Connect, transactions
|
||||||
|
│ │ └── (moved from smoothschedule/payments/)
|
||||||
|
│ └── tickets/ # Support tickets, email integration
|
||||||
|
│ └── (moved from smoothschedule/tickets/)
|
||||||
|
│
|
||||||
|
└── platform/ # Platform Administration
|
||||||
|
├── __init__.py
|
||||||
|
├── admin/ # Platform settings, subscriptions (renamed)
|
||||||
|
│ └── (moved from smoothschedule/platform_admin/)
|
||||||
|
└── api/ # Public API v1 (renamed from public_api)
|
||||||
|
└── (moved from smoothschedule/smoothschedule/public_api/)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Constraints
|
||||||
|
|
||||||
|
### 1. Migration History Preservation
|
||||||
|
|
||||||
|
Django migrations contain the app label in their `dependencies` and `app_label` references. We MUST:
|
||||||
|
|
||||||
|
- **Keep `app_label` unchanged** in each app's `Meta` class
|
||||||
|
- Update `AppConfig.name` to the new dotted path
|
||||||
|
- Django will use the `app_label` (not the path) for migration tracking
|
||||||
|
|
||||||
|
### 2. Foreign Key String References
|
||||||
|
|
||||||
|
Models use string references like `'users.User'` and `'core.Tenant'`. These reference `app_label`, not the module path, so they remain valid.
|
||||||
|
|
||||||
|
### 3. Import Path Updates
|
||||||
|
|
||||||
|
All imports across the codebase must be updated:
|
||||||
|
- `from core.models import Tenant` → `from smoothschedule.identity.core.models import Tenant`
|
||||||
|
- `from schedule.models import Event` → `from smoothschedule.scheduling.schedule.models import Event`
|
||||||
|
|
||||||
|
### 4. URL Configuration
|
||||||
|
|
||||||
|
`config/urls.py` imports views directly - all import paths must be updated.
|
||||||
|
|
||||||
|
### 5. Settings Files
|
||||||
|
|
||||||
|
- `config/settings/base.py` - `LOCAL_APPS`
|
||||||
|
- `config/settings/multitenancy.py` - `SHARED_APPS`, `TENANT_APPS`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Preparation (Serial)
|
||||||
|
|
||||||
|
**Agent 1: Setup & Verification**
|
||||||
|
1. Create all domain package directories with `__init__.py` files
|
||||||
|
2. Verify Docker is running and database is accessible
|
||||||
|
3. Run existing tests to establish baseline
|
||||||
|
4. Create backup of current migration state
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create domain packages
|
||||||
|
mkdir -p smoothschedule/smoothschedule/identity
|
||||||
|
mkdir -p smoothschedule/smoothschedule/scheduling
|
||||||
|
mkdir -p smoothschedule/smoothschedule/communication
|
||||||
|
mkdir -p smoothschedule/smoothschedule/commerce
|
||||||
|
mkdir -p smoothschedule/smoothschedule/platform
|
||||||
|
|
||||||
|
# Create __init__.py files
|
||||||
|
touch smoothschedule/smoothschedule/identity/__init__.py
|
||||||
|
touch smoothschedule/smoothschedule/scheduling/__init__.py
|
||||||
|
touch smoothschedule/smoothschedule/communication/__init__.py
|
||||||
|
touch smoothschedule/smoothschedule/commerce/__init__.py
|
||||||
|
touch smoothschedule/smoothschedule/platform/__init__.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Move Apps (Parallel - 5 Agents)
|
||||||
|
|
||||||
|
Each agent handles one domain. For each app move:
|
||||||
|
|
||||||
|
1. **Move directory** to new location
|
||||||
|
2. **Update `apps.py`** - change `name` to new dotted path, keep `label` same
|
||||||
|
3. **Update internal imports** within the app
|
||||||
|
4. **Add explicit `app_label`** to all model Meta classes (if not present)
|
||||||
|
|
||||||
|
#### Agent 2: Identity Domain
|
||||||
|
Move and update:
|
||||||
|
- `smoothschedule/core/` → `smoothschedule/smoothschedule/identity/core/`
|
||||||
|
- `smoothschedule/smoothschedule/users/` → `smoothschedule/smoothschedule/identity/users/`
|
||||||
|
|
||||||
|
**apps.py changes:**
|
||||||
|
```python
|
||||||
|
# identity/core/apps.py
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
name = "smoothschedule.identity.core" # NEW
|
||||||
|
label = "core" # KEEP SAME
|
||||||
|
verbose_name = "Core"
|
||||||
|
|
||||||
|
# identity/users/apps.py
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
name = "smoothschedule.identity.users" # NEW
|
||||||
|
label = "users" # KEEP SAME
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Agent 3: Scheduling Domain
|
||||||
|
Move and update:
|
||||||
|
- `smoothschedule/schedule/` → `smoothschedule/smoothschedule/scheduling/schedule/`
|
||||||
|
- `smoothschedule/contracts/` → `smoothschedule/smoothschedule/scheduling/contracts/`
|
||||||
|
- `smoothschedule/analytics/` → `smoothschedule/smoothschedule/scheduling/analytics/`
|
||||||
|
|
||||||
|
#### Agent 4: Communication Domain
|
||||||
|
Move and update:
|
||||||
|
- `smoothschedule/notifications/` → `smoothschedule/smoothschedule/communication/notifications/`
|
||||||
|
- `smoothschedule/smoothschedule/comms_credits/` → `smoothschedule/smoothschedule/communication/credits/`
|
||||||
|
- `smoothschedule/smoothschedule/field_mobile/` → `smoothschedule/smoothschedule/communication/mobile/`
|
||||||
|
- `smoothschedule/communication/` → `smoothschedule/smoothschedule/communication/messaging/`
|
||||||
|
|
||||||
|
**Note:** Rename apps for clarity:
|
||||||
|
- `comms_credits` label stays same, path changes
|
||||||
|
- `field_mobile` label stays same, path changes
|
||||||
|
- `communication` label stays same, path changes
|
||||||
|
|
||||||
|
#### Agent 5: Commerce Domain
|
||||||
|
Move and update:
|
||||||
|
- `smoothschedule/payments/` → `smoothschedule/smoothschedule/commerce/payments/`
|
||||||
|
- `smoothschedule/tickets/` → `smoothschedule/smoothschedule/commerce/tickets/`
|
||||||
|
|
||||||
|
#### Agent 6: Platform Domain
|
||||||
|
Move and update:
|
||||||
|
- `smoothschedule/platform_admin/` → `smoothschedule/smoothschedule/platform/admin/`
|
||||||
|
- `smoothschedule/smoothschedule/public_api/` → `smoothschedule/smoothschedule/platform/api/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Update Settings (Serial)
|
||||||
|
|
||||||
|
**Agent 7: Settings Configuration**
|
||||||
|
|
||||||
|
Update `config/settings/base.py`:
|
||||||
|
```python
|
||||||
|
LOCAL_APPS = [
|
||||||
|
# Identity
|
||||||
|
"smoothschedule.identity.users",
|
||||||
|
"smoothschedule.identity.core",
|
||||||
|
|
||||||
|
# Scheduling
|
||||||
|
"smoothschedule.scheduling.schedule",
|
||||||
|
"smoothschedule.scheduling.contracts",
|
||||||
|
"smoothschedule.scheduling.analytics",
|
||||||
|
|
||||||
|
# Communication
|
||||||
|
"smoothschedule.communication.notifications",
|
||||||
|
"smoothschedule.communication.credits",
|
||||||
|
"smoothschedule.communication.mobile",
|
||||||
|
"smoothschedule.communication.messaging",
|
||||||
|
|
||||||
|
# Commerce
|
||||||
|
"smoothschedule.commerce.payments",
|
||||||
|
"smoothschedule.commerce.tickets",
|
||||||
|
|
||||||
|
# Platform
|
||||||
|
"smoothschedule.platform.admin",
|
||||||
|
"smoothschedule.platform.api",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `config/settings/multitenancy.py`:
|
||||||
|
```python
|
||||||
|
SHARED_APPS = [
|
||||||
|
'django_tenants',
|
||||||
|
'smoothschedule.identity.core',
|
||||||
|
'smoothschedule.platform.admin',
|
||||||
|
# ... rest of shared apps with new paths
|
||||||
|
]
|
||||||
|
|
||||||
|
TENANT_APPS = [
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'smoothschedule.scheduling.schedule',
|
||||||
|
'smoothschedule.commerce.payments',
|
||||||
|
'smoothschedule.scheduling.contracts',
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Update All Import Paths (Parallel - Multiple Agents)
|
||||||
|
|
||||||
|
**This is the largest task.** Each agent handles specific import patterns:
|
||||||
|
|
||||||
|
#### Agent 8: Core Imports
|
||||||
|
Find and replace across entire codebase:
|
||||||
|
- `from core.models import` → `from smoothschedule.identity.core.models import`
|
||||||
|
- `from core.` → `from smoothschedule.identity.core.`
|
||||||
|
- `import core` → `import smoothschedule.identity.core as core`
|
||||||
|
|
||||||
|
#### Agent 9: Schedule Imports
|
||||||
|
- `from schedule.models import` → `from smoothschedule.scheduling.schedule.models import`
|
||||||
|
- `from schedule.` → `from smoothschedule.scheduling.schedule.`
|
||||||
|
|
||||||
|
#### Agent 10: Users/Auth Imports
|
||||||
|
- `from smoothschedule.users.` → `from smoothschedule.identity.users.`
|
||||||
|
- `from users.` → `from smoothschedule.identity.users.`
|
||||||
|
|
||||||
|
#### Agent 11: Other App Imports
|
||||||
|
Handle remaining apps:
|
||||||
|
- payments, tickets, notifications, contracts, analytics
|
||||||
|
- platform_admin, public_api, comms_credits, field_mobile, communication
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: URL Configuration Updates (Serial)
|
||||||
|
|
||||||
|
**Agent 12: URL Updates**
|
||||||
|
|
||||||
|
Update `config/urls.py` with new import paths:
|
||||||
|
```python
|
||||||
|
# Old
|
||||||
|
from schedule.views import ResourceViewSet, EventViewSet
|
||||||
|
from core.api_views import business_current
|
||||||
|
|
||||||
|
# New
|
||||||
|
from smoothschedule.scheduling.schedule.views import ResourceViewSet, EventViewSet
|
||||||
|
from smoothschedule.identity.core.api_views import business_current
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 6: Cleanup & Verification (Serial)
|
||||||
|
|
||||||
|
**Agent 13: Cleanup**
|
||||||
|
1. Remove old empty directories at top level
|
||||||
|
2. Remove deprecated `smoothschedule/smoothschedule/schedule/` directory
|
||||||
|
3. Update `CLAUDE.md` documentation
|
||||||
|
4. Update any remaining references
|
||||||
|
|
||||||
|
**Agent 14: Verification**
|
||||||
|
1. Run `docker compose exec django python manage.py check`
|
||||||
|
2. Run `docker compose exec django python manage.py makemigrations --check`
|
||||||
|
3. Run `docker compose exec django python manage.py migrate --check`
|
||||||
|
4. Run test suite
|
||||||
|
5. Manual smoke test of key endpoints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## App Label Mapping Reference
|
||||||
|
|
||||||
|
| Old Import Path | New Import Path | app_label (unchanged) |
|
||||||
|
|----------------|-----------------|----------------------|
|
||||||
|
| `core` | `smoothschedule.identity.core` | `core` |
|
||||||
|
| `smoothschedule.users` | `smoothschedule.identity.users` | `users` |
|
||||||
|
| `schedule` | `smoothschedule.scheduling.schedule` | `schedule` |
|
||||||
|
| `contracts` | `smoothschedule.scheduling.contracts` | `contracts` |
|
||||||
|
| `analytics` | `smoothschedule.scheduling.analytics` | `analytics` |
|
||||||
|
| `notifications` | `smoothschedule.communication.notifications` | `notifications` |
|
||||||
|
| `smoothschedule.comms_credits` | `smoothschedule.communication.credits` | `comms_credits` |
|
||||||
|
| `smoothschedule.field_mobile` | `smoothschedule.communication.mobile` | `field_mobile` |
|
||||||
|
| `communication` | `smoothschedule.communication.messaging` | `communication` |
|
||||||
|
| `payments` | `smoothschedule.commerce.payments` | `payments` |
|
||||||
|
| `tickets` | `smoothschedule.commerce.tickets` | `tickets` |
|
||||||
|
| `platform_admin` | `smoothschedule.platform.admin` | `platform_admin` |
|
||||||
|
| `smoothschedule.public_api` | `smoothschedule.platform.api` | `public_api` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If issues are encountered:
|
||||||
|
|
||||||
|
1. **Git Reset:** `git checkout main` and delete branch
|
||||||
|
2. **Database:** No migration changes, database remains intact
|
||||||
|
3. **Docker:** Rebuild containers if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] All apps moved to domain-based structure
|
||||||
|
- [ ] `python manage.py check` passes
|
||||||
|
- [ ] `python manage.py makemigrations --check` shows no changes
|
||||||
|
- [ ] All existing tests pass
|
||||||
|
- [ ] Frontend can communicate with API
|
||||||
|
- [ ] Mobile app can communicate with API
|
||||||
|
- [ ] CLAUDE.md updated with new structure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Order
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1 (Serial): Agent 1 - Setup
|
||||||
|
Phase 2 (Parallel): Agents 2-6 - Move apps by domain
|
||||||
|
Phase 3 (Serial): Agent 7 - Update settings
|
||||||
|
Phase 4 (Parallel): Agents 8-11 - Update imports
|
||||||
|
Phase 5 (Serial): Agent 12 - URL updates
|
||||||
|
Phase 6 (Serial): Agents 13-14 - Cleanup & verify
|
||||||
|
```
|
||||||
|
|
||||||
|
**Total Agents:** 14 (8 can run in parallel at peak)
|
||||||
179
PLAN_HELP_DOCS.md
Normal file
179
PLAN_HELP_DOCS.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Help Documentation Implementation Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This plan covers creating comprehensive help documentation for the SmoothSchedule business dashboard, adding contextual help buttons to each page, and creating a monolithic help document.
|
||||||
|
|
||||||
|
## Phase 1: Create Plugin Page First (User Request)
|
||||||
|
|
||||||
|
### Task 1.1: Create CreatePlugin.tsx Page
|
||||||
|
- Create `/frontend/src/pages/CreatePlugin.tsx`
|
||||||
|
- Features:
|
||||||
|
- Name, description, short description fields
|
||||||
|
- Category dropdown (EMAIL, REPORTS, CUSTOMER, BOOKING, INTEGRATION, AUTOMATION, OTHER)
|
||||||
|
- Plugin code editor with syntax highlighting (using same Prism setup as HelpPluginDocs)
|
||||||
|
- Template variables preview (auto-extracted from code)
|
||||||
|
- Version field (default 1.0.0)
|
||||||
|
- Logo URL field (optional)
|
||||||
|
- Save as Private / Submit to Marketplace options
|
||||||
|
- Visibility selector (PRIVATE, PUBLIC)
|
||||||
|
- Uses API endpoint: `POST /api/plugin-templates/`
|
||||||
|
- Plan feature gate: `can_create_plugins`
|
||||||
|
|
||||||
|
### Task 1.2: Add Route for CreatePlugin
|
||||||
|
- Add lazy import: `const CreatePlugin = React.lazy(() => import('./pages/CreatePlugin'));`
|
||||||
|
- Add route: `/plugins/create` pointing to CreatePlugin component
|
||||||
|
|
||||||
|
## Phase 2: Create Reusable HelpButton Component
|
||||||
|
|
||||||
|
### Task 2.1: Create HelpButton Component
|
||||||
|
- Create `/frontend/src/components/HelpButton.tsx`
|
||||||
|
- Props: `helpPath: string` (route to help page)
|
||||||
|
- Renders: HelpCircle icon button at fixed position (top-right of page)
|
||||||
|
- Styling: Circular button with question mark icon, tooltip on hover
|
||||||
|
- Uses Link from react-router-dom to navigate to help page
|
||||||
|
|
||||||
|
## Phase 3: Create Individual Help Pages
|
||||||
|
|
||||||
|
### 3.1 Core Pages Help
|
||||||
|
| Page | Help File | Route |
|
||||||
|
|------|-----------|-------|
|
||||||
|
| Dashboard | HelpDashboard.tsx | /help/dashboard |
|
||||||
|
| Scheduler | HelpScheduler.tsx | /help/scheduler |
|
||||||
|
| Tasks | HelpTasks.tsx | /help/tasks |
|
||||||
|
|
||||||
|
### 3.2 Manage Section Help
|
||||||
|
| Page | Help File | Route |
|
||||||
|
|------|-----------|-------|
|
||||||
|
| Customers | HelpCustomers.tsx | /help/customers |
|
||||||
|
| Services | HelpServices.tsx | /help/services |
|
||||||
|
| Resources | HelpResources.tsx | /help/resources |
|
||||||
|
| Staff | HelpStaff.tsx | /help/staff |
|
||||||
|
|
||||||
|
### 3.3 Communicate Section Help
|
||||||
|
| Page | Help File | Route |
|
||||||
|
|------|-----------|-------|
|
||||||
|
| Messages | HelpMessages.tsx | /help/messages |
|
||||||
|
| Tickets | HelpTicketing.tsx (exists) | /help/ticketing |
|
||||||
|
|
||||||
|
### 3.4 Money Section Help
|
||||||
|
| Page | Help File | Route |
|
||||||
|
|------|-----------|-------|
|
||||||
|
| Payments | HelpPayments.tsx | /help/payments |
|
||||||
|
|
||||||
|
### 3.5 Extend Section Help
|
||||||
|
| Page | Help File | Route |
|
||||||
|
|------|-----------|-------|
|
||||||
|
| Plugins | HelpPluginsOverview.tsx | /help/plugins-overview |
|
||||||
|
| Plugin Marketplace | (link to existing HelpPluginDocs) | /help/plugins |
|
||||||
|
| My Plugins | HelpMyPlugins.tsx | /help/my-plugins |
|
||||||
|
| Create Plugin | HelpCreatePlugin.tsx | /help/create-plugin |
|
||||||
|
|
||||||
|
### 3.6 Settings Section Help
|
||||||
|
| Page | Help File | Route |
|
||||||
|
|------|-----------|-------|
|
||||||
|
| General | HelpSettingsGeneral.tsx | /help/settings/general |
|
||||||
|
| Resource Types | HelpSettingsResourceTypes.tsx | /help/settings/resource-types |
|
||||||
|
| Booking | HelpSettingsBooking.tsx | /help/settings/booking |
|
||||||
|
| Appearance | HelpSettingsAppearance.tsx | /help/settings/appearance |
|
||||||
|
| Email Templates | HelpSettingsEmailTemplates.tsx | /help/settings/email-templates |
|
||||||
|
| Custom Domains | HelpSettingsCustomDomains.tsx | /help/settings/custom-domains |
|
||||||
|
| API & Webhooks | HelpSettingsApi.tsx | /help/settings/api |
|
||||||
|
| Authentication | HelpSettingsAuth.tsx | /help/settings/authentication |
|
||||||
|
| Email Setup | HelpEmailSettings.tsx (exists) | /help/email-settings |
|
||||||
|
| SMS & Calling | HelpSettingsSmsCalling.tsx | /help/settings/sms-calling |
|
||||||
|
| Plan & Billing | HelpSettingsBilling.tsx | /help/settings/billing |
|
||||||
|
| Quota Management | HelpSettingsQuota.tsx | /help/settings/quota |
|
||||||
|
|
||||||
|
## Phase 4: Add HelpButton to Each Page
|
||||||
|
|
||||||
|
Add the HelpButton component to the top-right of each dashboard page, linking to its corresponding help page.
|
||||||
|
|
||||||
|
## Phase 5: Update HelpPluginDocs
|
||||||
|
|
||||||
|
### Task 5.1: Review and Update Plugin Documentation
|
||||||
|
- Verify plugin documentation matches current codebase
|
||||||
|
- Add section for "Creating Custom Plugins"
|
||||||
|
- Add links to API documentation
|
||||||
|
- Ensure examples work with current API
|
||||||
|
|
||||||
|
## Phase 6: Create Monolithic Help Document
|
||||||
|
|
||||||
|
### Task 6.1: Create HelpGuideComplete.tsx
|
||||||
|
- Compile all help content into single comprehensive page
|
||||||
|
- Table of contents with anchor links
|
||||||
|
- Searchable content
|
||||||
|
- Organized by sections (Core, Manage, Communicate, Money, Extend, Settings)
|
||||||
|
|
||||||
|
### Task 6.2: Update HelpGuide.tsx
|
||||||
|
- Replace "Coming Soon" with actual compiled documentation
|
||||||
|
- Or redirect to HelpGuideComplete
|
||||||
|
|
||||||
|
## Phase 7: Register All Routes
|
||||||
|
|
||||||
|
Add all new help page routes to App.tsx in the business dashboard section.
|
||||||
|
|
||||||
|
## Help Page Template Structure
|
||||||
|
|
||||||
|
Each help page should follow this structure:
|
||||||
|
```tsx
|
||||||
|
- Header with icon and title
|
||||||
|
- Overview/Introduction
|
||||||
|
- Key Features section
|
||||||
|
- How to Use section (step-by-step)
|
||||||
|
- Benefits section
|
||||||
|
- Tips & Best Practices
|
||||||
|
- Related Features (links to other help pages)
|
||||||
|
- Need More Help? (link to support/tickets)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
1. Create CreatePlugin.tsx page and route
|
||||||
|
2. Create HelpButton component
|
||||||
|
3. Create help pages for core pages (Dashboard, Scheduler, Tasks)
|
||||||
|
4. Create help pages for Manage section
|
||||||
|
5. Create help pages for Communicate section
|
||||||
|
6. Create help pages for Money section
|
||||||
|
7. Create help pages for Extend section (including plugin docs update)
|
||||||
|
8. Create help pages for Settings section
|
||||||
|
9. Add HelpButton to all pages
|
||||||
|
10. Create monolithic help document
|
||||||
|
11. Test all help pages and navigation
|
||||||
|
|
||||||
|
## Files to Create
|
||||||
|
|
||||||
|
### New Components
|
||||||
|
- `/frontend/src/components/HelpButton.tsx`
|
||||||
|
|
||||||
|
### New Pages
|
||||||
|
- `/frontend/src/pages/CreatePlugin.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpDashboard.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpScheduler.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpTasks.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpCustomers.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpServices.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpResources.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpStaff.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpMessages.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpPayments.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpPluginsOverview.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpMyPlugins.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpCreatePlugin.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpSettingsGeneral.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpSettingsResourceTypes.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpSettingsBooking.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpSettingsAppearance.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpSettingsEmailTemplates.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpSettingsCustomDomains.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpSettingsApi.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpSettingsAuth.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpSettingsSmsCalling.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpSettingsBilling.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpSettingsQuota.tsx`
|
||||||
|
- `/frontend/src/pages/help/HelpGuideComplete.tsx`
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
- `/frontend/src/App.tsx` - Add routes
|
||||||
|
- `/frontend/src/pages/HelpPluginDocs.tsx` - Update with current codebase info
|
||||||
|
- `/frontend/src/pages/HelpGuide.tsx` - Replace Coming Soon
|
||||||
|
- All dashboard pages - Add HelpButton component
|
||||||
653
PLAN_MULTI_EMAIL_TICKETING.md
Normal file
653
PLAN_MULTI_EMAIL_TICKETING.md
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
# Implementation Plan: Multi-Email Ticketing System
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Add support for multiple email addresses per business in the ticketing system, with color-coded visual indicators and per-email IMAP/SMTP configuration.
|
||||||
|
|
||||||
|
## Current System Analysis
|
||||||
|
|
||||||
|
### Existing Components
|
||||||
|
|
||||||
|
1. **Django Backend (`tickets` app)**
|
||||||
|
- `Ticket` model: Core ticket entity
|
||||||
|
- `TicketComment` model: Ticket responses
|
||||||
|
- `TicketEmailSettings` model: **Singleton** platform-wide email config
|
||||||
|
- `IncomingTicketEmail` model: Email audit log
|
||||||
|
- `TicketEmailReceiver` class: IMAP email fetching
|
||||||
|
- `TicketEmailService` class: SMTP email sending
|
||||||
|
|
||||||
|
2. **Frontend**
|
||||||
|
- `Tickets.tsx`: Main ticket listing page
|
||||||
|
- `TicketModal.tsx`: Ticket detail modal
|
||||||
|
- `useTickets` hook: Fetch tickets
|
||||||
|
- `useTicketEmailSettings` hook: Manage email settings (singleton)
|
||||||
|
- `Settings.tsx`: Business settings page
|
||||||
|
|
||||||
|
3. **Current Email Flow**
|
||||||
|
- Single email account configured platform-wide
|
||||||
|
- Emails matched to tickets by ID in subject/address
|
||||||
|
- Comments created from email replies
|
||||||
|
- New tickets created from unmatched emails
|
||||||
|
|
||||||
|
## Requirements (from user clarification)
|
||||||
|
|
||||||
|
1. **Per-Business Email Addresses**
|
||||||
|
- Each business provides their own email account(s) and credentials
|
||||||
|
- Multiple email addresses per business
|
||||||
|
- Each email has independent IMAP/SMTP settings
|
||||||
|
|
||||||
|
2. **Email Address Properties**
|
||||||
|
- Display name (e.g., "Support", "Billing")
|
||||||
|
- Email address
|
||||||
|
- IMAP settings (host, port, username, password, SSL)
|
||||||
|
- SMTP settings (host, port, username, password, TLS/SSL)
|
||||||
|
- Color for visual identification (hex color code)
|
||||||
|
- Active/inactive status
|
||||||
|
|
||||||
|
3. **Ticket Routing**
|
||||||
|
- Incoming emails matched to business by email address configuration
|
||||||
|
- Reply emails matched to existing tickets
|
||||||
|
- New emails create tickets for that business
|
||||||
|
- System attempts to match sender email to customer/staff in business
|
||||||
|
|
||||||
|
4. **UI Requirements**
|
||||||
|
- Colored left border on ticket rows indicating source email
|
||||||
|
- Business settings page to manage email addresses
|
||||||
|
- Test connection buttons for IMAP/SMTP
|
||||||
|
- Email address selector when creating tickets manually
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Django Backend Models
|
||||||
|
|
||||||
|
#### 1.1 Create `TicketEmailAddress` Model
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/models.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TicketEmailAddress(models.Model):
|
||||||
|
"""
|
||||||
|
Per-business email address configuration for ticket management.
|
||||||
|
Each business can have multiple email addresses with their own settings.
|
||||||
|
"""
|
||||||
|
tenant = models.ForeignKey(
|
||||||
|
Tenant,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='ticket_email_addresses',
|
||||||
|
help_text="Business this email address belongs to"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Display information
|
||||||
|
display_name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
help_text="Display name (e.g., 'Support', 'Billing', 'Sales')"
|
||||||
|
)
|
||||||
|
email_address = models.EmailField(
|
||||||
|
help_text="Email address for sending/receiving tickets"
|
||||||
|
)
|
||||||
|
color = models.CharField(
|
||||||
|
max_length=7,
|
||||||
|
default='#3b82f6',
|
||||||
|
help_text="Hex color code for visual identification (e.g., #3b82f6)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# IMAP settings (inbound)
|
||||||
|
imap_host = models.CharField(max_length=255)
|
||||||
|
imap_port = models.IntegerField(default=993)
|
||||||
|
imap_use_ssl = models.BooleanField(default=True)
|
||||||
|
imap_username = models.CharField(max_length=255)
|
||||||
|
imap_password = models.CharField(max_length=255) # Encrypted in production
|
||||||
|
imap_folder = models.CharField(max_length=100, default='INBOX')
|
||||||
|
|
||||||
|
# SMTP settings (outbound)
|
||||||
|
smtp_host = models.CharField(max_length=255)
|
||||||
|
smtp_port = models.IntegerField(default=587)
|
||||||
|
smtp_use_tls = models.BooleanField(default=True)
|
||||||
|
smtp_use_ssl = models.BooleanField(default=False)
|
||||||
|
smtp_username = models.CharField(max_length=255)
|
||||||
|
smtp_password = models.CharField(max_length=255) # Encrypted in production
|
||||||
|
|
||||||
|
# Status and tracking
|
||||||
|
is_active = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Whether this email address is actively checked"
|
||||||
|
)
|
||||||
|
is_default = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Default email for new tickets in this business"
|
||||||
|
)
|
||||||
|
last_check_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
last_error = models.TextField(blank=True, default='')
|
||||||
|
emails_processed_count = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-is_default', 'display_name']
|
||||||
|
unique_together = [['tenant', 'email_address']]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['tenant', 'is_active']),
|
||||||
|
models.Index(fields=['email_address']),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.display_name} <{self.email_address}> ({self.tenant.name})"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# Ensure only one default per tenant
|
||||||
|
if self.is_default:
|
||||||
|
TicketEmailAddress.objects.filter(
|
||||||
|
tenant=self.tenant,
|
||||||
|
is_default=True
|
||||||
|
).exclude(pk=self.pk).update(is_default=False)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 Update `Ticket` Model
|
||||||
|
|
||||||
|
Add field to track which email address received/sent the ticket:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Ticket(models.Model):
|
||||||
|
# ... existing fields ...
|
||||||
|
|
||||||
|
source_email_address = models.ForeignKey(
|
||||||
|
'TicketEmailAddress',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='tickets',
|
||||||
|
help_text="Email address this ticket was received from or sent to"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3 Update `IncomingTicketEmail` Model
|
||||||
|
|
||||||
|
Add field to track which email address received the email:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IncomingTicketEmail(models.Model):
|
||||||
|
# ... existing fields ...
|
||||||
|
|
||||||
|
email_address = models.ForeignKey(
|
||||||
|
'TicketEmailAddress',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='incoming_emails',
|
||||||
|
help_text="Email address configuration that received this email"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Django Backend Logic
|
||||||
|
|
||||||
|
#### 2.1 Update Email Receiver
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/email_receiver.py`
|
||||||
|
|
||||||
|
- Modify `TicketEmailReceiver` to iterate through all active `TicketEmailAddress` objects
|
||||||
|
- Connect to each email address's IMAP server
|
||||||
|
- Process emails for each address
|
||||||
|
- Associate processed tickets with the source email address
|
||||||
|
|
||||||
|
#### 2.2 Update Email Sender
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/email_notifications.py`
|
||||||
|
|
||||||
|
- Modify `TicketEmailService` to use the ticket's `source_email_address` for sending
|
||||||
|
- Fall back to business's default email address if none specified
|
||||||
|
|
||||||
|
### Phase 3: Django Backend API
|
||||||
|
|
||||||
|
#### 3.1 Create Serializers
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/serializers.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TicketEmailAddressSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = TicketEmailAddress
|
||||||
|
fields = [
|
||||||
|
'id', 'tenant', 'display_name', 'email_address', 'color',
|
||||||
|
'imap_host', 'imap_port', 'imap_use_ssl', 'imap_username',
|
||||||
|
'imap_password', 'imap_folder',
|
||||||
|
'smtp_host', 'smtp_port', 'smtp_use_tls', 'smtp_use_ssl',
|
||||||
|
'smtp_username', 'smtp_password',
|
||||||
|
'is_active', 'is_default', 'last_check_at', 'last_error',
|
||||||
|
'emails_processed_count', 'created_at', 'updated_at'
|
||||||
|
]
|
||||||
|
read_only_fields = ['tenant', 'last_check_at', 'last_error',
|
||||||
|
'emails_processed_count', 'created_at', 'updated_at']
|
||||||
|
extra_kwargs = {
|
||||||
|
'imap_password': {'write_only': True},
|
||||||
|
'smtp_password': {'write_only': True},
|
||||||
|
}
|
||||||
|
|
||||||
|
class TicketEmailAddressListSerializer(serializers.ModelSerializer):
|
||||||
|
"""Lightweight serializer without passwords"""
|
||||||
|
class Meta:
|
||||||
|
model = TicketEmailAddress
|
||||||
|
fields = [
|
||||||
|
'id', 'display_name', 'email_address', 'color',
|
||||||
|
'is_active', 'is_default', 'last_check_at',
|
||||||
|
'emails_processed_count'
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `TicketSerializer` to include email address:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TicketSerializer(serializers.ModelSerializer):
|
||||||
|
# ... existing fields ...
|
||||||
|
source_email_address = TicketEmailAddressListSerializer(read_only=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 Create ViewSet
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/views.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TicketEmailAddressViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
ViewSet for managing ticket email addresses.
|
||||||
|
Only business owners and managers can manage email addresses.
|
||||||
|
"""
|
||||||
|
serializer_class = TicketEmailAddressSerializer
|
||||||
|
permission_classes = [IsTenantUser]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
# Business users see their own email addresses
|
||||||
|
if user.role in ['owner', 'manager', 'staff']:
|
||||||
|
return TicketEmailAddress.objects.filter(
|
||||||
|
tenant=user.tenant
|
||||||
|
)
|
||||||
|
# Platform users see all
|
||||||
|
elif user.role in ['superuser', 'platform_manager']:
|
||||||
|
return TicketEmailAddress.objects.all()
|
||||||
|
return TicketEmailAddress.objects.none()
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action == 'list':
|
||||||
|
return TicketEmailAddressListSerializer
|
||||||
|
return TicketEmailAddressSerializer
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
# Automatically set tenant from current user
|
||||||
|
serializer.save(tenant=self.request.user.tenant)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def test_imap(self, request, pk=None):
|
||||||
|
"""Test IMAP connection for this email address"""
|
||||||
|
email_address = self.get_object()
|
||||||
|
# Test IMAP connection logic
|
||||||
|
return Response({'status': 'success'})
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def test_smtp(self, request, pk=None):
|
||||||
|
"""Test SMTP connection for this email address"""
|
||||||
|
email_address = self.get_object()
|
||||||
|
# Test SMTP connection logic
|
||||||
|
return Response({'status': 'success'})
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def fetch_now(self, request, pk=None):
|
||||||
|
"""Manually trigger email fetch for this address"""
|
||||||
|
email_address = self.get_object()
|
||||||
|
# Trigger email fetch
|
||||||
|
return Response({'status': 'fetching'})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3 Add URL Routes
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/smoothschedule/tickets/urls.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
router.register(r'email-addresses', views.TicketEmailAddressViewSet, basename='ticketemailaddress')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Frontend - React Hooks
|
||||||
|
|
||||||
|
#### 4.1 Create API Client Functions
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/api/ticketEmailAddresses.ts` (new file)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface TicketEmailAddress {
|
||||||
|
id: number;
|
||||||
|
display_name: string;
|
||||||
|
email_address: string;
|
||||||
|
color: string;
|
||||||
|
imap_host: string;
|
||||||
|
imap_port: number;
|
||||||
|
imap_use_ssl: boolean;
|
||||||
|
imap_username: string;
|
||||||
|
imap_password?: string;
|
||||||
|
imap_folder: string;
|
||||||
|
smtp_host: string;
|
||||||
|
smtp_port: number;
|
||||||
|
smtp_use_tls: boolean;
|
||||||
|
smtp_use_ssl: boolean;
|
||||||
|
smtp_username: string;
|
||||||
|
smtp_password?: string;
|
||||||
|
is_active: boolean;
|
||||||
|
is_default: boolean;
|
||||||
|
last_check_at?: string;
|
||||||
|
last_error?: string;
|
||||||
|
emails_processed_count: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TicketEmailAddressCreate = Omit<TicketEmailAddress, 'id' | 'last_check_at' | 'last_error' | 'emails_processed_count' | 'created_at' | 'updated_at'>;
|
||||||
|
|
||||||
|
export const getTicketEmailAddresses = async (): Promise<TicketEmailAddress[]> => {
|
||||||
|
const response = await apiClient.get('/tickets/email-addresses/');
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTicketEmailAddress = async (data: TicketEmailAddressCreate): Promise<TicketEmailAddress> => {
|
||||||
|
const response = await apiClient.post('/tickets/email-addresses/', data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTicketEmailAddress = async (id: number, data: Partial<TicketEmailAddressCreate>): Promise<TicketEmailAddress> => {
|
||||||
|
const response = await apiClient.patch(`/tickets/email-addresses/${id}/`, data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTicketEmailAddress = async (id: number): Promise<void> => {
|
||||||
|
await apiClient.delete(`/tickets/email-addresses/${id}/`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testImapConnection = async (id: number): Promise<{ status: string; message?: string }> => {
|
||||||
|
const response = await apiClient.post(`/tickets/email-addresses/${id}/test_imap/`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testSmtpConnection = async (id: number): Promise<{ status: string; message?: string }> => {
|
||||||
|
const response = await apiClient.post(`/tickets/email-addresses/${id}/test_smtp/`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchEmailsNow = async (id: number): Promise<{ status: string }> => {
|
||||||
|
const response = await apiClient.post(`/tickets/email-addresses/${id}/fetch_now/`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 Create React Query Hooks
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/hooks/useTicketEmailAddresses.ts` (new file)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
getTicketEmailAddresses,
|
||||||
|
createTicketEmailAddress,
|
||||||
|
updateTicketEmailAddress,
|
||||||
|
deleteTicketEmailAddress,
|
||||||
|
testImapConnection,
|
||||||
|
testSmtpConnection,
|
||||||
|
fetchEmailsNow,
|
||||||
|
TicketEmailAddress,
|
||||||
|
TicketEmailAddressCreate,
|
||||||
|
} from '../api/ticketEmailAddresses';
|
||||||
|
|
||||||
|
const QUERY_KEY = 'ticketEmailAddresses';
|
||||||
|
|
||||||
|
export const useTicketEmailAddresses = () => {
|
||||||
|
return useQuery<TicketEmailAddress[]>({
|
||||||
|
queryKey: [QUERY_KEY],
|
||||||
|
queryFn: getTicketEmailAddresses,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateTicketEmailAddress = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: TicketEmailAddressCreate) => createTicketEmailAddress(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateTicketEmailAddress = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, data }: { id: number; data: Partial<TicketEmailAddressCreate> }) =>
|
||||||
|
updateTicketEmailAddress(id, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteTicketEmailAddress = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => deleteTicketEmailAddress(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTestImapConnection = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => testImapConnection(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTestSmtpConnection = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => testSmtpConnection(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFetchEmailsNow = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => fetchEmailsNow(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5: Frontend - React Components
|
||||||
|
|
||||||
|
#### 5.1 Email Address Management Component
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/components/TicketEmailAddressManager.tsx` (new file)
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- List all email addresses for the business
|
||||||
|
- Add new email address
|
||||||
|
- Edit existing email address
|
||||||
|
- Delete email address
|
||||||
|
- Test IMAP/SMTP connections
|
||||||
|
- Set default email address
|
||||||
|
- Color picker for visual identification
|
||||||
|
- Enable/disable email addresses
|
||||||
|
|
||||||
|
#### 5.2 Update Ticket List UI
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/pages/Tickets.tsx`
|
||||||
|
|
||||||
|
Modify ticket rows to include colored left border:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div
|
||||||
|
className="ticket-row"
|
||||||
|
style={{
|
||||||
|
borderLeft: ticket.source_email_address
|
||||||
|
? `4px solid ${ticket.source_email_address.color}`
|
||||||
|
: '4px solid transparent'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Ticket content */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.3 Update Types
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/types.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface TicketEmailAddress {
|
||||||
|
id: number;
|
||||||
|
display_name: string;
|
||||||
|
email_address: string;
|
||||||
|
color: string;
|
||||||
|
is_active: boolean;
|
||||||
|
is_default: boolean;
|
||||||
|
last_check_at?: string;
|
||||||
|
emails_processed_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ticket {
|
||||||
|
// ... existing fields ...
|
||||||
|
source_email_address?: TicketEmailAddress;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.4 Add to Business Settings
|
||||||
|
|
||||||
|
**File:** `/home/poduck/Desktop/smoothschedule2/frontend/src/pages/Settings.tsx`
|
||||||
|
|
||||||
|
Add new tab for "Email Addresses" that renders `TicketEmailAddressManager` component.
|
||||||
|
|
||||||
|
### Phase 6: Database Migration
|
||||||
|
|
||||||
|
#### 6.1 Create Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py makemigrations tickets
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2 Run Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py migrate tickets
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3 Data Migration (if needed)
|
||||||
|
|
||||||
|
If there's existing `TicketEmailSettings` data, create a data migration to convert it to `TicketEmailAddress` records for each tenant.
|
||||||
|
|
||||||
|
### Phase 7: Testing
|
||||||
|
|
||||||
|
#### 7.1 Backend Tests
|
||||||
|
|
||||||
|
- Test email address CRUD operations
|
||||||
|
- Test email receiver with multiple addresses
|
||||||
|
- Test email sender using correct source address
|
||||||
|
- Test tenant isolation
|
||||||
|
|
||||||
|
#### 7.2 Frontend Tests
|
||||||
|
|
||||||
|
- Test email address list rendering
|
||||||
|
- Test add/edit/delete operations
|
||||||
|
- Test connection testing UI
|
||||||
|
- Test ticket list color borders
|
||||||
|
|
||||||
|
### Phase 8: Documentation
|
||||||
|
|
||||||
|
#### 8.1 User Documentation
|
||||||
|
|
||||||
|
- How to add email addresses
|
||||||
|
- How to configure IMAP/SMTP settings
|
||||||
|
- How to test connections
|
||||||
|
- Color coding explanation
|
||||||
|
|
||||||
|
#### 8.2 Developer Documentation
|
||||||
|
|
||||||
|
- API endpoints documentation
|
||||||
|
- Model relationships
|
||||||
|
- Email processing flow
|
||||||
|
- Celery task schedule (if applicable)
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
### Option 1: Keep Legacy System (Recommended)
|
||||||
|
|
||||||
|
- Keep `TicketEmailSettings` for platform-level configuration
|
||||||
|
- New `TicketEmailAddress` for per-business configuration
|
||||||
|
- Businesses can opt-in to multi-email system
|
||||||
|
- Existing single-email businesses continue working
|
||||||
|
|
||||||
|
### Option 2: Full Migration
|
||||||
|
|
||||||
|
- Deprecate `TicketEmailSettings`
|
||||||
|
- Migrate all existing data to `TicketEmailAddress`
|
||||||
|
- All businesses use new system
|
||||||
|
|
||||||
|
**Recommendation:** Option 1 for backward compatibility
|
||||||
|
|
||||||
|
## Risks & Considerations
|
||||||
|
|
||||||
|
1. **Security**
|
||||||
|
- Email passwords stored in database (consider encryption)
|
||||||
|
- SMTP/IMAP credentials exposure risk
|
||||||
|
- Recommend OAuth2 for Gmail/Outlook in future
|
||||||
|
|
||||||
|
2. **Performance**
|
||||||
|
- Multiple IMAP connections may increase load
|
||||||
|
- Consider Celery task queue for email fetching
|
||||||
|
- Implement rate limiting
|
||||||
|
|
||||||
|
3. **Email Deliverability**
|
||||||
|
- Each business responsible for their own SPF/DKIM records
|
||||||
|
- No centralized email reputation management
|
||||||
|
|
||||||
|
4. **UI/UX**
|
||||||
|
- Color picker needs to be user-friendly
|
||||||
|
- Color accessibility (contrast ratio)
|
||||||
|
- Mobile responsiveness
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **OAuth2 Support**
|
||||||
|
- Google Workspace integration
|
||||||
|
- Microsoft 365 integration
|
||||||
|
|
||||||
|
2. **Email Templates Per Address**
|
||||||
|
- Different signatures per email address
|
||||||
|
- Custom auto-responses
|
||||||
|
|
||||||
|
3. **Analytics**
|
||||||
|
- Email volume by address
|
||||||
|
- Response time by address
|
||||||
|
|
||||||
|
4. **Auto-Assignment**
|
||||||
|
- Route tickets to specific staff based on email address
|
||||||
|
|
||||||
|
## Implementation Timeline
|
||||||
|
|
||||||
|
- **Phase 1-2 (Backend Models & Logic):** 2-3 days
|
||||||
|
- **Phase 3 (Backend API):** 1-2 days
|
||||||
|
- **Phase 4-5 (Frontend):** 3-4 days
|
||||||
|
- **Phase 6-7 (Migration & Testing):** 1-2 days
|
||||||
|
- **Phase 8 (Documentation):** 1 day
|
||||||
|
|
||||||
|
**Total Estimated Time:** 8-12 days
|
||||||
|
|
||||||
|
## Approval Required
|
||||||
|
|
||||||
|
Before proceeding with implementation, please confirm:
|
||||||
|
|
||||||
|
1. ✅ Per-business email addresses (not platform-wide)
|
||||||
|
2. ✅ Businesses provide their own IMAP/SMTP credentials
|
||||||
|
3. ✅ Colored left border for visual identification
|
||||||
|
4. ✅ Email address management in business settings (not platform dashboard)
|
||||||
|
5. ⚠️ Security approach for storing email passwords
|
||||||
|
6. ⚠️ Migration strategy (keep legacy vs full migration)
|
||||||
|
|
||||||
|
## Questions for Product Owner
|
||||||
|
|
||||||
|
1. Should we encrypt email passwords in the database?
|
||||||
|
2. Do we need email address approval workflow (platform admin approval)?
|
||||||
|
3. Should there be a limit on number of email addresses per business?
|
||||||
|
4. Do we need email forwarding (forward to another address)?
|
||||||
|
5. Should unmatched emails (not tied to a ticket) create new tickets or be ignored?
|
||||||
296
PRODUCTION-READY.md
Normal file
296
PRODUCTION-READY.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# SmoothSchedule Production Readiness Report
|
||||||
|
|
||||||
|
## Status: READY FOR DEPLOYMENT ✓
|
||||||
|
|
||||||
|
This document confirms that SmoothSchedule is fully configured and ready for production deployment.
|
||||||
|
|
||||||
|
## Configuration Complete ✓
|
||||||
|
|
||||||
|
### 1. DigitalOcean Spaces Configuration ✓
|
||||||
|
- **Access Key ID:** DO801P4R8QXYMY4CE8WZ
|
||||||
|
- **Secret Access Key:** Configured
|
||||||
|
- **Bucket Name:** smoothschedule
|
||||||
|
- **Region:** nyc3
|
||||||
|
- **Endpoint:** https://nyc3.digitaloceanspaces.com
|
||||||
|
|
||||||
|
**Status:** Environment variables configured in `smoothschedule/.envs/.production/.django`
|
||||||
|
|
||||||
|
### 2. Backend (Django) ✓
|
||||||
|
- **Framework:** Django 5.2.8
|
||||||
|
- **Storage:** django-storages with S3 backend (DigitalOcean Spaces)
|
||||||
|
- **Database:** PostgreSQL with multi-tenancy support
|
||||||
|
- **Task Queue:** Celery with Redis
|
||||||
|
- **Web Server:** Gunicorn behind Traefik
|
||||||
|
- **SSL/HTTPS:** Let's Encrypt automatic certificates via Traefik
|
||||||
|
|
||||||
|
**Production Settings:**
|
||||||
|
- ✓ SECRET_KEY configured
|
||||||
|
- ✓ ALLOWED_HOSTS set to `.smoothschedule.com`
|
||||||
|
- ✓ DEBUG=False (production mode)
|
||||||
|
- ✓ Static files → DigitalOcean Spaces
|
||||||
|
- ✓ Media files → DigitalOcean Spaces
|
||||||
|
- ✓ Security headers configured
|
||||||
|
- ✓ HTTPS redirect enabled
|
||||||
|
|
||||||
|
### 3. Frontend (React) ✓
|
||||||
|
- **Framework:** React 18 with Vite
|
||||||
|
- **Build:** Production build ready
|
||||||
|
- **API Endpoint:** https://smoothschedule.com/api
|
||||||
|
- **Multi-tenant:** Subdomain-based routing
|
||||||
|
|
||||||
|
**Production Settings:**
|
||||||
|
- ✓ API URL configured for production
|
||||||
|
- ✓ Build optimization enabled
|
||||||
|
|
||||||
|
### 4. Docker Configuration ✓
|
||||||
|
**Services:**
|
||||||
|
- ✓ Django (Gunicorn)
|
||||||
|
- ✓ PostgreSQL
|
||||||
|
- ✓ Redis
|
||||||
|
- ✓ Traefik (reverse proxy + SSL)
|
||||||
|
- ✓ Celery Worker
|
||||||
|
- ✓ Celery Beat (scheduler)
|
||||||
|
- ✓ Flower (Celery monitoring)
|
||||||
|
|
||||||
|
**Production Compose:** `docker-compose.production.yml`
|
||||||
|
|
||||||
|
### 5. SSL/HTTPS ✓
|
||||||
|
- **Provider:** Let's Encrypt
|
||||||
|
- **Auto-renewal:** Enabled via Traefik
|
||||||
|
- **Domains:**
|
||||||
|
- smoothschedule.com
|
||||||
|
- www.smoothschedule.com
|
||||||
|
- platform.smoothschedule.com
|
||||||
|
- api.smoothschedule.com
|
||||||
|
- *.smoothschedule.com (wildcard for tenants)
|
||||||
|
|
||||||
|
### 6. Security ✓
|
||||||
|
- ✓ HTTPS enforced
|
||||||
|
- ✓ Secure cookies
|
||||||
|
- ✓ CSRF protection
|
||||||
|
- ✓ Random secret keys
|
||||||
|
- ✓ Database password protected
|
||||||
|
- ✓ Flower dashboard password protected
|
||||||
|
|
||||||
|
## Deployment Scripts Created ✓
|
||||||
|
|
||||||
|
### 1. `server-setup.sh`
|
||||||
|
**Purpose:** Initial server setup (run once)
|
||||||
|
**Installs:**
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- AWS CLI (for Spaces management)
|
||||||
|
- UFW firewall
|
||||||
|
- Fail2ban
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
ssh poduck@smoothschedule.com 'bash -s' < server-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `setup-spaces.sh`
|
||||||
|
**Purpose:** Create and configure DigitalOcean Spaces bucket
|
||||||
|
**Actions:**
|
||||||
|
- Creates bucket
|
||||||
|
- Sets public-read ACL
|
||||||
|
- Configures CORS
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
ssh poduck@smoothschedule.com
|
||||||
|
./setup-spaces.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. `deploy.sh`
|
||||||
|
**Purpose:** Full deployment pipeline
|
||||||
|
**Actions:**
|
||||||
|
- Builds frontend
|
||||||
|
- Uploads code to server
|
||||||
|
- Builds Docker images
|
||||||
|
- Runs migrations
|
||||||
|
- Collects static files
|
||||||
|
- Starts services
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
./deploy.sh poduck@smoothschedule.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation Created ✓
|
||||||
|
|
||||||
|
### 1. DEPLOYMENT.md
|
||||||
|
Comprehensive deployment guide covering:
|
||||||
|
- Prerequisites
|
||||||
|
- Step-by-step deployment
|
||||||
|
- DNS configuration
|
||||||
|
- SSL setup
|
||||||
|
- Troubleshooting
|
||||||
|
- Maintenance
|
||||||
|
|
||||||
|
### 2. CLAUDE.md (Updated)
|
||||||
|
Added production deployment section with:
|
||||||
|
- Quick deploy commands
|
||||||
|
- Production URLs
|
||||||
|
- Management commands
|
||||||
|
- Environment variables
|
||||||
|
|
||||||
|
## What You Need to Do Before Deploying
|
||||||
|
|
||||||
|
### Prerequisites Checklist
|
||||||
|
|
||||||
|
#### 1. Server Access
|
||||||
|
- [ ] Ensure you can SSH to: `poduck@smoothschedule.com`
|
||||||
|
- [ ] Verify sudo password: `chaff/starry`
|
||||||
|
|
||||||
|
#### 2. DNS Configuration
|
||||||
|
Configure these DNS records at your domain registrar:
|
||||||
|
|
||||||
|
```
|
||||||
|
Type Name Value TTL
|
||||||
|
A smoothschedule.com YOUR_SERVER_IP 300
|
||||||
|
A *.smoothschedule.com YOUR_SERVER_IP 300
|
||||||
|
CNAME www smoothschedule.com 300
|
||||||
|
```
|
||||||
|
|
||||||
|
**To find YOUR_SERVER_IP:**
|
||||||
|
```bash
|
||||||
|
ping smoothschedule.com
|
||||||
|
# or
|
||||||
|
ssh poduck@smoothschedule.com 'curl -4 ifconfig.me'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Server Firewall Ports
|
||||||
|
Ensure these ports are open on your server:
|
||||||
|
- [ ] Port 22 (SSH)
|
||||||
|
- [ ] Port 80 (HTTP)
|
||||||
|
- [ ] Port 443 (HTTPS)
|
||||||
|
- [ ] Port 5555 (Flower - optional)
|
||||||
|
|
||||||
|
#### 4. DigitalOcean Spaces
|
||||||
|
- [ ] Create bucket (run `setup-spaces.sh` on server)
|
||||||
|
- [ ] Verify credentials are correct
|
||||||
|
|
||||||
|
## Deployment Steps (Quick Start)
|
||||||
|
|
||||||
|
### Step 1: Initial Server Setup (One-Time)
|
||||||
|
```bash
|
||||||
|
# From your local machine
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2
|
||||||
|
|
||||||
|
# Run server setup
|
||||||
|
ssh poduck@smoothschedule.com 'bash -s' < server-setup.sh
|
||||||
|
|
||||||
|
# Note: You'll need to logout/login after this for Docker group changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Setup DigitalOcean Spaces (One-Time)
|
||||||
|
```bash
|
||||||
|
# Copy setup script to server
|
||||||
|
scp setup-spaces.sh poduck@smoothschedule.com:~/
|
||||||
|
|
||||||
|
# SSH to server and run it
|
||||||
|
ssh poduck@smoothschedule.com
|
||||||
|
./setup-spaces.sh
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Deploy Application
|
||||||
|
```bash
|
||||||
|
# From your local machine
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2
|
||||||
|
./deploy.sh poduck@smoothschedule.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Post-Deployment Setup
|
||||||
|
```bash
|
||||||
|
# SSH to server
|
||||||
|
ssh poduck@smoothschedule.com
|
||||||
|
cd ~/smoothschedule
|
||||||
|
|
||||||
|
# Create superuser
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py createsuperuser
|
||||||
|
|
||||||
|
# Create a business tenant
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in the Django shell:
|
||||||
|
```python
|
||||||
|
from core.models import Business
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
# Create a business
|
||||||
|
business = Business.objects.create(
|
||||||
|
name="Demo Business",
|
||||||
|
subdomain="demo",
|
||||||
|
schema_name="demo",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create business owner
|
||||||
|
owner = User.objects.create_user(
|
||||||
|
username="demo_owner",
|
||||||
|
email="owner@demo.com",
|
||||||
|
password="choose_a_password",
|
||||||
|
role="owner",
|
||||||
|
business_subdomain="demo"
|
||||||
|
)
|
||||||
|
|
||||||
|
exit()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Verify Deployment
|
||||||
|
Visit these URLs in your browser:
|
||||||
|
- https://smoothschedule.com - Main site
|
||||||
|
- https://platform.smoothschedule.com - Platform dashboard
|
||||||
|
- https://demo.smoothschedule.com - Demo business
|
||||||
|
- https://smoothschedule.com:5555 - Flower (Celery monitoring)
|
||||||
|
|
||||||
|
## Monitoring & Maintenance
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
```bash
|
||||||
|
ssh poduck@smoothschedule.com
|
||||||
|
cd ~/smoothschedule
|
||||||
|
docker compose -f docker-compose.production.yml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Status
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.production.yml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart Services
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.production.yml restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update/Redeploy
|
||||||
|
Simply run the deploy script again:
|
||||||
|
```bash
|
||||||
|
./deploy.sh poduck@smoothschedule.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support & Troubleshooting
|
||||||
|
|
||||||
|
See [DEPLOYMENT.md](DEPLOYMENT.md) for:
|
||||||
|
- Detailed troubleshooting steps
|
||||||
|
- SSL certificate issues
|
||||||
|
- Database connection problems
|
||||||
|
- Static files not loading
|
||||||
|
- Celery task issues
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
✅ **Production Configuration:** Complete
|
||||||
|
✅ **DigitalOcean Spaces:** Configured
|
||||||
|
✅ **Docker Setup:** Ready
|
||||||
|
✅ **SSL/HTTPS:** Automatic via Traefik
|
||||||
|
✅ **Deployment Scripts:** Created
|
||||||
|
✅ **Documentation:** Complete
|
||||||
|
|
||||||
|
**Next Action:** Run the deployment steps above to go live!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Questions?** See DEPLOYMENT.md or check the logs on the server.
|
||||||
602
PRODUCTION_DEPLOYMENT.md
Normal file
602
PRODUCTION_DEPLOYMENT.md
Normal file
@@ -0,0 +1,602 @@
|
|||||||
|
# SmoothSchedule - Production Deployment Manual
|
||||||
|
|
||||||
|
Complete step-by-step guide for manually deploying SmoothSchedule from scratch on a production server. This guide is useful when you need to reset the entire production deployment or troubleshoot deployment issues.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Prerequisites](#prerequisites)
|
||||||
|
2. [Complete Fresh Deployment](#complete-fresh-deployment)
|
||||||
|
3. [Docker Build & Startup](#docker-build--startup)
|
||||||
|
4. [Database Initialization](#database-initialization)
|
||||||
|
5. [Static Files & Migrations](#static-files--migrations)
|
||||||
|
6. [Verification Steps](#verification-steps)
|
||||||
|
7. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Required on Production Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check these are installed and running
|
||||||
|
docker --version # Docker 20.10+
|
||||||
|
docker compose --version # Docker Compose 2.0+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Locally (Before Deployment)
|
||||||
|
|
||||||
|
1. All code changes committed to git (`main` branch)
|
||||||
|
2. All missing files added to git and pushed
|
||||||
|
3. Production environment variables backed up locally
|
||||||
|
4. `.envs/.production/` directory exists with proper credentials
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Fresh Deployment
|
||||||
|
|
||||||
|
### Step 1: Save Production Environment Variables Locally
|
||||||
|
|
||||||
|
**On your local development machine:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup current production environment variables
|
||||||
|
scp poduck@smoothschedule.com:~/smoothschedule/smoothschedule/.envs/.production/.django \
|
||||||
|
/tmp/production_django_env_backup
|
||||||
|
|
||||||
|
scp poduck@smoothschedule.com:~/smoothschedule/smoothschedule/.envs/.production/.postgres \
|
||||||
|
/tmp/production_postgres_env_backup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Prepare & Commit All Code Changes Locally
|
||||||
|
|
||||||
|
**On your local development machine:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2
|
||||||
|
|
||||||
|
# Check for any missing files
|
||||||
|
git status
|
||||||
|
|
||||||
|
# Add all changes and new files
|
||||||
|
git add -A
|
||||||
|
|
||||||
|
# Review staged changes
|
||||||
|
git diff --cached
|
||||||
|
|
||||||
|
# Commit changes
|
||||||
|
git commit -m "Production deployment: Add missing files and config updates"
|
||||||
|
|
||||||
|
# Push to main branch
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Bring Down Production Containers
|
||||||
|
|
||||||
|
**SSH into production server:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh poduck@smoothschedule.com
|
||||||
|
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# Stop all containers (preserves volumes)
|
||||||
|
docker compose -f docker-compose.production.yml down
|
||||||
|
|
||||||
|
# Or to completely reset and remove volumes (careful!):
|
||||||
|
docker compose -f docker-compose.production.yml down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Remove Production Codebase
|
||||||
|
|
||||||
|
**On production server:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~
|
||||||
|
|
||||||
|
# Remove everything
|
||||||
|
rm -rf smoothschedule
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Pull Fresh Code from Git
|
||||||
|
|
||||||
|
**On production server:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~
|
||||||
|
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://git.talova.net/poduck/smoothschedule.git smoothschedule
|
||||||
|
|
||||||
|
cd smoothschedule
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** If git authentication fails, configure credentials on the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config --global user.email "poduck@smoothschedule.com"
|
||||||
|
git config --global user.name "Poduck"
|
||||||
|
|
||||||
|
# Or store credentials
|
||||||
|
git credential approve
|
||||||
|
# Then paste: protocol=https
|
||||||
|
# host=git.talova.net
|
||||||
|
# username=poduck
|
||||||
|
# password=chaff/starry
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Restore Production Environment Variables
|
||||||
|
|
||||||
|
**On production server:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# Create the .envs/.production directory if it doesn't exist
|
||||||
|
mkdir -p .envs/.production
|
||||||
|
|
||||||
|
# Restore from backups on local machine or recreate them
|
||||||
|
# Option A: Use SCP to copy from local machine
|
||||||
|
scp /tmp/production_django_env_backup poduck@smoothschedule.com:~/smoothschedule/smoothschedule/.envs/.production/.django
|
||||||
|
scp /tmp/production_postgres_env_backup poduck@smoothschedule.com:~/smoothschedule/smoothschedule/.envs/.production/.postgres
|
||||||
|
|
||||||
|
# Option B: Manually recreate the files on the production server
|
||||||
|
# (see Environment Variables section below)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker Build & Startup
|
||||||
|
|
||||||
|
### Step 7: Build Docker Images
|
||||||
|
|
||||||
|
**On production server:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# Build all production Docker images
|
||||||
|
docker compose -f docker-compose.production.yml build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected output:**
|
||||||
|
```
|
||||||
|
✓ Successfully built:
|
||||||
|
- smoothschedule_production_django
|
||||||
|
- smoothschedule_production_celeryworker
|
||||||
|
- smoothschedule_production_celerybeat
|
||||||
|
- smoothschedule_production_flower
|
||||||
|
- smoothschedule_production_postgres
|
||||||
|
- smoothschedule_production_traefik
|
||||||
|
- smoothschedule-awscli
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Start Containers
|
||||||
|
|
||||||
|
**On production server:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# Start all containers in detached mode
|
||||||
|
docker compose -f docker-compose.production.yml up -d
|
||||||
|
|
||||||
|
# Wait for services to stabilize (10-15 seconds)
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# Check container status
|
||||||
|
docker compose -f docker-compose.production.yml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected status:** All containers should be `Up` or `Healthy`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Initialization
|
||||||
|
|
||||||
|
### Step 9: Run Database Migrations
|
||||||
|
|
||||||
|
**On production server:**
|
||||||
|
|
||||||
|
The application uses django-tenants for multi-tenant support. Migrations must be run in this order:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# 1. Migrate the public/shared schema (all shared apps)
|
||||||
|
docker compose -f docker-compose.production.yml exec -T django \
|
||||||
|
python manage.py migrate_schemas --shared
|
||||||
|
|
||||||
|
# Expected output should show multiple migrations applied to "public" schema
|
||||||
|
```
|
||||||
|
|
||||||
|
**If you get "service django is not running":**
|
||||||
|
|
||||||
|
The Django container may not be fully started. Wait a bit longer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sleep 30 # Wait for Django to initialize
|
||||||
|
docker compose -f docker-compose.production.yml exec -T django \
|
||||||
|
python manage.py migrate_schemas --shared
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Static Files & Migrations
|
||||||
|
|
||||||
|
### Step 10: Collect Static Files
|
||||||
|
|
||||||
|
**On production server:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
docker compose -f docker-compose.production.yml exec -T django \
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
```
|
||||||
|
|
||||||
|
This collects all static files (CSS, JS, images) and uploads them to:
|
||||||
|
- **Local filesystem:** `smoothschedule/staticfiles/`
|
||||||
|
- **DigitalOcean Spaces/S3:** Configured via `DJANGO_AWS_*` environment variables
|
||||||
|
|
||||||
|
### Step 11: Create Superuser (First Time Only)
|
||||||
|
|
||||||
|
**On production server, if this is a fresh installation:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
docker compose -f docker-compose.production.yml exec django \
|
||||||
|
python manage.py createsuperuser
|
||||||
|
|
||||||
|
# Follow the prompts to create the admin user
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 12: Create Initial Tenant (First Time Only)
|
||||||
|
|
||||||
|
**On production server, if you need a demo tenant:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in the Django shell:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.models import Tenant, Domain
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
|
# Create a tenant
|
||||||
|
tenant = Tenant.objects.create(
|
||||||
|
name="Demo Company",
|
||||||
|
slug="demo",
|
||||||
|
is_free_trial=False,
|
||||||
|
is_temporary=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a domain for the tenant
|
||||||
|
Domain.objects.create(
|
||||||
|
domain="demo.smoothschedule.com",
|
||||||
|
tenant=tenant,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Created tenant: {tenant.name} with domain: demo.smoothschedule.com")
|
||||||
|
exit()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Steps
|
||||||
|
|
||||||
|
### Step 13: Verify All Services Are Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# Check container status
|
||||||
|
docker compose -f docker-compose.production.yml ps
|
||||||
|
|
||||||
|
# View logs for any errors
|
||||||
|
docker compose -f docker-compose.production.yml logs --tail=50
|
||||||
|
|
||||||
|
# Check specific service logs
|
||||||
|
docker compose -f docker-compose.production.yml logs django --tail=30
|
||||||
|
docker compose -f docker-compose.production.yml logs postgres --tail=30
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 14: Test API Endpoints
|
||||||
|
|
||||||
|
**From your local machine:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test the backend API
|
||||||
|
curl -s "https://smoothschedule.com/api/health/" | jq
|
||||||
|
|
||||||
|
# Test tenant access
|
||||||
|
curl -s "https://demo.smoothschedule.com/api/resources/" | jq
|
||||||
|
|
||||||
|
# Test platform admin
|
||||||
|
curl -s "https://platform.smoothschedule.com/api/admin/businesses/" | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 15: Verify Nginx Frontend
|
||||||
|
|
||||||
|
The frontend should be accessible at:
|
||||||
|
- **Main site:** `https://smoothschedule.com`
|
||||||
|
- **Platform dashboard:** `https://platform.smoothschedule.com`
|
||||||
|
- **Tenant subdomain:** `https://demo.smoothschedule.com`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables Reference
|
||||||
|
|
||||||
|
### Django Configuration (`.envs/.production/.django`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Security & Secrets
|
||||||
|
DJANGO_SECRET_KEY=your-secret-key-here
|
||||||
|
DJANGO_DEBUG=False
|
||||||
|
DJANGO_ALLOWED_HOSTS=smoothschedule.com,platform.smoothschedule.com,*.smoothschedule.com
|
||||||
|
|
||||||
|
# Admin & URLs
|
||||||
|
DJANGO_ADMIN_URL=<random-slug>/
|
||||||
|
FRONTEND_URL=https://platform.smoothschedule.com
|
||||||
|
PLATFORM_BASE_URL=https://platform.smoothschedule.com
|
||||||
|
|
||||||
|
# Celery & Redis
|
||||||
|
REDIS_URL=redis://redis:6379/0
|
||||||
|
CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
|
|
||||||
|
# AWS/DigitalOcean Spaces (for media storage)
|
||||||
|
DJANGO_AWS_ACCESS_KEY_ID=your-access-key
|
||||||
|
DJANGO_AWS_SECRET_ACCESS_KEY=your-secret-key
|
||||||
|
DJANGO_AWS_STORAGE_BUCKET_NAME=smoothschedule
|
||||||
|
DJANGO_AWS_S3_ENDPOINT_URL=https://nyc3.digitaloceanspaces.com
|
||||||
|
DJANGO_AWS_S3_REGION_NAME=nyc3
|
||||||
|
DJANGO_AWS_S3_CUSTOM_DOMAIN=smoothschedule.nyc3.digitaloceanspaces.com
|
||||||
|
|
||||||
|
# Email Configuration (optional)
|
||||||
|
MAILGUN_API_KEY=your-mailgun-key
|
||||||
|
MAILGUN_DOMAIN=mg.smoothschedule.com
|
||||||
|
|
||||||
|
# SSL/Security
|
||||||
|
DJANGO_SECURE_SSL_REDIRECT=True
|
||||||
|
DJANGO_SECURE_HSTS_SECONDS=60
|
||||||
|
DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=True
|
||||||
|
DJANGO_SECURE_HSTS_PRELOAD=True
|
||||||
|
DJANGO_SESSION_COOKIE_SECURE=True
|
||||||
|
DJANGO_CSRF_COOKIE_SECURE=True
|
||||||
|
|
||||||
|
# Sentry (optional - error tracking)
|
||||||
|
SENTRY_DSN=
|
||||||
|
SENTRY_ENVIRONMENT=production
|
||||||
|
SENTRY_TRACES_SAMPLE_RATE=0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL Configuration (`.envs/.production/.postgres`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_DB=smoothschedule
|
||||||
|
POSTGRES_USER=<random-username>
|
||||||
|
POSTGRES_PASSWORD=<random-secure-password>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Django Container Won't Start
|
||||||
|
|
||||||
|
**Symptom:** `docker compose ps` shows Django as exited
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# View full error logs
|
||||||
|
docker compose -f docker-compose.production.yml logs django --tail=100
|
||||||
|
|
||||||
|
# Check if database is accessible
|
||||||
|
docker compose -f docker-compose.production.yml logs postgres --tail=20
|
||||||
|
|
||||||
|
# Restart Django after fixing the issue
|
||||||
|
docker compose -f docker-compose.production.yml restart django
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Fails with "role does not exist"
|
||||||
|
|
||||||
|
**Symptom:** Error when running `migrate_schemas --shared`:
|
||||||
|
```
|
||||||
|
FATAL: role "postgres" does not exist
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** The database volume is corrupted. Reset and restart:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# Stop and remove all volumes
|
||||||
|
docker compose -f docker-compose.production.yml down -v
|
||||||
|
|
||||||
|
# Restart (this will recreate the database)
|
||||||
|
docker compose -f docker-compose.production.yml up -d
|
||||||
|
|
||||||
|
# Wait for database to initialize (30 seconds)
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
# Try migrations again
|
||||||
|
docker compose -f docker-compose.production.yml exec -T django \
|
||||||
|
python manage.py migrate_schemas --shared
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cannot Connect to Production Server
|
||||||
|
|
||||||
|
**Check SSH access:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh poduck@smoothschedule.com "echo 'Connection successful'"
|
||||||
|
```
|
||||||
|
|
||||||
|
**If that fails:**
|
||||||
|
- Verify your SSH key is authorized on the server
|
||||||
|
- Check firewall rules allow SSH (port 22)
|
||||||
|
- Verify DNS resolves `smoothschedule.com` to the correct IP
|
||||||
|
|
||||||
|
### Traefik Certificate Issues
|
||||||
|
|
||||||
|
**Symptom:** SSL certificate errors, HTTP redirects failing
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# Check Traefik logs
|
||||||
|
docker compose -f docker-compose.production.yml logs traefik --tail=50
|
||||||
|
|
||||||
|
# Restart Traefik to request new certificates
|
||||||
|
docker compose -f docker-compose.production.yml restart traefik
|
||||||
|
|
||||||
|
# Wait for Let's Encrypt to issue certificates (5-10 minutes)
|
||||||
|
# Check via: https://smoothschedule.com (should show valid cert)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Not Loading
|
||||||
|
|
||||||
|
**Verify nginx container is running:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if nginx image was built
|
||||||
|
docker images | grep smoothschedule
|
||||||
|
|
||||||
|
# If missing, rebuild
|
||||||
|
docker compose -f docker-compose.production.yml build nginx
|
||||||
|
|
||||||
|
# Restart the service
|
||||||
|
docker compose -f docker-compose.production.yml restart
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker logs $(docker ps -q --filter "name=nginx")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH to production server
|
||||||
|
ssh poduck@smoothschedule.com
|
||||||
|
|
||||||
|
# Navigate to project
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# View all container logs
|
||||||
|
docker compose -f docker-compose.production.yml logs -f
|
||||||
|
|
||||||
|
# View Django logs only
|
||||||
|
docker compose -f docker-compose.production.yml logs django -f --tail=100
|
||||||
|
|
||||||
|
# Stop all containers
|
||||||
|
docker compose -f docker-compose.production.yml down
|
||||||
|
|
||||||
|
# Start all containers
|
||||||
|
docker compose -f docker-compose.production.yml up -d
|
||||||
|
|
||||||
|
# Restart a specific service
|
||||||
|
docker compose -f docker-compose.production.yml restart django
|
||||||
|
|
||||||
|
# Run a Django management command
|
||||||
|
docker compose -f docker-compose.production.yml exec -T django python manage.py <command>
|
||||||
|
|
||||||
|
# Access Django shell
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py shell
|
||||||
|
|
||||||
|
# Create a database backup
|
||||||
|
docker compose -f docker-compose.production.yml exec -T postgres \
|
||||||
|
pg_dump -U $POSTGRES_USER smoothschedule > backup.sql
|
||||||
|
|
||||||
|
# Execute arbitrary SQL
|
||||||
|
docker compose -f docker-compose.production.yml exec -T postgres \
|
||||||
|
psql -U $POSTGRES_USER smoothschedule -c "SELECT version();"
|
||||||
|
|
||||||
|
# Check Docker resource usage
|
||||||
|
docker stats
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced: Rollback Procedure
|
||||||
|
|
||||||
|
If a deployment fails catastrophically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Stop everything
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
docker compose -f docker-compose.production.yml down
|
||||||
|
|
||||||
|
# 2. Restore from database backup (if available)
|
||||||
|
docker compose -f docker-compose.production.yml exec -T postgres \
|
||||||
|
psql -U $POSTGRES_USER smoothschedule < backup.sql
|
||||||
|
|
||||||
|
# 3. Checkout previous git commit
|
||||||
|
cd ~/smoothschedule
|
||||||
|
git checkout <previous-commit-hash>
|
||||||
|
|
||||||
|
# 4. Rebuild and restart
|
||||||
|
cd smoothschedule
|
||||||
|
docker compose -f docker-compose.production.yml build
|
||||||
|
docker compose -f docker-compose.production.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring Checklist
|
||||||
|
|
||||||
|
After deployment, monitor these for 24 hours:
|
||||||
|
|
||||||
|
- [ ] No error logs in `docker compose logs django`
|
||||||
|
- [ ] API endpoints respond with 200 status
|
||||||
|
- [ ] SSL certificates are valid (https://smoothschedule.com)
|
||||||
|
- [ ] Frontend loads without console errors
|
||||||
|
- [ ] Tenant subdomains work correctly
|
||||||
|
- [ ] Static files are being served (CSS, JS load)
|
||||||
|
- [ ] Background tasks execute (check Celery worker logs)
|
||||||
|
- [ ] No database connection errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
1. **Always backup before major changes:**
|
||||||
|
- Database: `pg_dump`
|
||||||
|
- Environment variables: Copy `.envs/.production/` locally
|
||||||
|
- Code: Git commits
|
||||||
|
|
||||||
|
2. **Never modify code on production directly:**
|
||||||
|
- Change code locally → Git push → Production git pull
|
||||||
|
- Only edit `.env` files directly on production
|
||||||
|
|
||||||
|
3. **Multi-Tenancy Considerations:**
|
||||||
|
- Each tenant gets a separate PostgreSQL schema
|
||||||
|
- Migrations apply to "public" schema (shared) first
|
||||||
|
- Tenant migrations happen automatically on first request to new tenant
|
||||||
|
|
||||||
|
4. **Docker Compose File:**
|
||||||
|
- Always use `-f docker-compose.production.yml` for production
|
||||||
|
- Never use `-f docker-compose.local.yml` on production
|
||||||
|
|
||||||
|
5. **Persistence:**
|
||||||
|
- Database: `production_postgres_data` volume
|
||||||
|
- Redis: `production_redis_data` volume
|
||||||
|
- Traefik certs: `production_traefik` volume
|
||||||
|
- These are preserved when using `down` (not `down -v`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** December 2025
|
||||||
|
**Deployment Version:** Manual v1.0
|
||||||
175
QUICK-REFERENCE.md
Normal file
175
QUICK-REFERENCE.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# SmoothSchedule Quick Reference
|
||||||
|
|
||||||
|
## Deployment Commands
|
||||||
|
|
||||||
|
### Deploy to Production
|
||||||
|
```bash
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2
|
||||||
|
./deploy.sh poduck@smoothschedule.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSH to Server
|
||||||
|
```bash
|
||||||
|
ssh poduck@smoothschedule.com
|
||||||
|
# Password: chaff/starry
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Management
|
||||||
|
|
||||||
|
### Navigate to Project
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker compose -f docker-compose.production.yml logs -f
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker compose -f docker-compose.production.yml logs -f django
|
||||||
|
docker compose -f docker-compose.production.yml logs -f celeryworker
|
||||||
|
docker compose -f docker-compose.production.yml logs -f traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Status
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.production.yml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart Services
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker compose -f docker-compose.production.yml restart
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker compose -f docker-compose.production.yml restart django
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Django Commands
|
||||||
|
```bash
|
||||||
|
# Migrations
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py migrate
|
||||||
|
|
||||||
|
# Create superuser
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py createsuperuser
|
||||||
|
|
||||||
|
# Django shell
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py shell
|
||||||
|
|
||||||
|
# Collect static files
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py collectstatic --noinput
|
||||||
|
```
|
||||||
|
|
||||||
|
## URLs
|
||||||
|
|
||||||
|
- **Main Site:** https://smoothschedule.com
|
||||||
|
- **Platform Dashboard:** https://platform.smoothschedule.com
|
||||||
|
- **API:** https://smoothschedule.com/api
|
||||||
|
- **Admin:** https://smoothschedule.com/admin
|
||||||
|
- **Flower (Celery):** https://smoothschedule.com:5555
|
||||||
|
|
||||||
|
## DigitalOcean Spaces
|
||||||
|
|
||||||
|
### View Bucket Contents
|
||||||
|
```bash
|
||||||
|
aws --profile do-tor1 s3 ls s3://smoothschedule/
|
||||||
|
aws --profile do-tor1 s3 ls s3://smoothschedule/static/
|
||||||
|
aws --profile do-tor1 s3 ls s3://smoothschedule/media/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload File
|
||||||
|
```bash
|
||||||
|
aws --profile do-tor1 s3 cp file.jpg s3://smoothschedule/media/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Public URLs
|
||||||
|
- **Static:** https://smoothschedule.nyc3.digitaloceanspaces.com/static/
|
||||||
|
- **Media:** https://smoothschedule.nyc3.digitaloceanspaces.com/media/
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### 500 Error
|
||||||
|
```bash
|
||||||
|
# Check Django logs
|
||||||
|
docker compose -f docker-compose.production.yml logs django --tail=100
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL Not Working
|
||||||
|
```bash
|
||||||
|
# Check Traefik logs
|
||||||
|
docker compose -f docker-compose.production.yml logs traefik
|
||||||
|
|
||||||
|
# Verify DNS
|
||||||
|
dig smoothschedule.com +short
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Issues
|
||||||
|
```bash
|
||||||
|
# Check PostgreSQL
|
||||||
|
docker compose -f docker-compose.production.yml logs postgres
|
||||||
|
|
||||||
|
# Access database
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py dbshell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static Files Not Loading
|
||||||
|
```bash
|
||||||
|
# Re-collect static files
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
# Check Spaces
|
||||||
|
aws --profile do-tor1 s3 ls s3://smoothschedule/static/ | head
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backups
|
||||||
|
|
||||||
|
### Create Database Backup
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.production.yml exec postgres backup
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Backups
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.production.yml exec postgres backups
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Backup
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.production.yml exec postgres restore <backup_file>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Emergency Commands
|
||||||
|
|
||||||
|
### Stop All Services
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.production.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start All Services
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.production.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild Everything
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.production.yml down
|
||||||
|
docker compose -f docker-compose.production.yml build --no-cache
|
||||||
|
docker compose -f docker-compose.production.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Resource Usage
|
||||||
|
```bash
|
||||||
|
docker stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Files
|
||||||
|
|
||||||
|
- **Backend:** `~/smoothschedule/.envs/.production/.django`
|
||||||
|
- **Database:** `~/smoothschedule/.envs/.production/.postgres`
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Detailed Guide:** See DEPLOYMENT.md
|
||||||
|
- **Production Status:** See PRODUCTION-READY.md
|
||||||
|
- **Main Docs:** See CLAUDE.md
|
||||||
195
QUICK_REFERENCE_CALENDAR_SYNC.md
Normal file
195
QUICK_REFERENCE_CALENDAR_SYNC.md
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# Calendar Sync Permission - Quick Reference
|
||||||
|
|
||||||
|
## What Was Added
|
||||||
|
|
||||||
|
A permission gating system for calendar sync features in the Django backend.
|
||||||
|
|
||||||
|
## Key Components
|
||||||
|
|
||||||
|
### 1. Database Field
|
||||||
|
```python
|
||||||
|
# core/models.py - Added to Tenant model
|
||||||
|
can_use_calendar_sync = models.BooleanField(default=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Permission Check Factory
|
||||||
|
```python
|
||||||
|
# core/permissions.py - Added to FEATURE_NAMES
|
||||||
|
'can_use_calendar_sync': 'Calendar Sync',
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. OAuth Integration
|
||||||
|
```python
|
||||||
|
# core/oauth_views.py - Check when purpose is 'calendar'
|
||||||
|
if purpose == 'calendar':
|
||||||
|
calendar_permission = HasFeaturePermission('can_use_calendar_sync')
|
||||||
|
if not calendar_permission().has_permission(request, self):
|
||||||
|
return Response({'error': 'Feature not available'}, status=403)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Calendar Sync Views
|
||||||
|
```python
|
||||||
|
# schedule/calendar_sync_views.py
|
||||||
|
CalendarListView # GET /api/calendar/list/
|
||||||
|
CalendarSyncView # POST /api/calendar/sync/
|
||||||
|
CalendarDeleteView # DELETE /api/calendar/disconnect/
|
||||||
|
CalendarStatusView # GET /api/calendar/status/
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### Enable for a Tenant
|
||||||
|
```bash
|
||||||
|
# Via Django shell
|
||||||
|
from core.models import Tenant
|
||||||
|
tenant = Tenant.objects.get(schema_name='demo')
|
||||||
|
tenant.can_use_calendar_sync = True
|
||||||
|
tenant.save()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use in ViewSet
|
||||||
|
```python
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from core.permissions import HasFeaturePermission
|
||||||
|
|
||||||
|
class MyViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = [IsAuthenticated, HasFeaturePermission('can_use_calendar_sync')]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use in APIView
|
||||||
|
```python
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
class MyView(APIView):
|
||||||
|
permission_classes = [CalendarSyncPermission]
|
||||||
|
# CalendarSyncPermission = IsAuthenticated + has_feature check
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description | Permission |
|
||||||
|
|--------|----------|-------------|-----------|
|
||||||
|
| GET | /api/calendar/status/ | Check if calendar sync is available | Auth only |
|
||||||
|
| GET | /api/calendar/list/ | List connected calendars | Calendar sync |
|
||||||
|
| POST | /api/calendar/sync/ | Start calendar sync | Calendar sync |
|
||||||
|
| DELETE | /api/calendar/disconnect/ | Disconnect a calendar | Calendar sync |
|
||||||
|
| POST | /api/oauth/google/initiate/ | Start Google OAuth for calendar | Calendar sync (if purpose=calendar) |
|
||||||
|
| POST | /api/oauth/microsoft/initiate/ | Start MS OAuth for calendar | Calendar sync (if purpose=calendar) |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Run tests
|
||||||
|
```bash
|
||||||
|
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest schedule/tests/test_calendar_sync_permissions.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test endpoints manually
|
||||||
|
```bash
|
||||||
|
# Check status (always works)
|
||||||
|
curl http://lvh.me:8000/api/calendar/status/ -H "Authorization: Bearer <token>"
|
||||||
|
|
||||||
|
# List calendars (requires permission)
|
||||||
|
curl http://lvh.me:8000/api/calendar/list/ -H "Authorization: Bearer <token>"
|
||||||
|
# Returns 403 if permission not granted
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| core/models.py | Added can_use_calendar_sync field |
|
||||||
|
| core/permissions.py | Added to FEATURE_NAMES |
|
||||||
|
| core/oauth_views.py | Added permission check for calendar |
|
||||||
|
|
||||||
|
## Files Created
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| core/migrations/0016_tenant_can_use_calendar_sync.py | Database migration |
|
||||||
|
| schedule/calendar_sync_views.py | Calendar sync API views |
|
||||||
|
| schedule/calendar_sync_urls.py | URL routing |
|
||||||
|
| schedule/tests/test_calendar_sync_permissions.py | Test suite |
|
||||||
|
| CALENDAR_SYNC_INTEGRATION.md | Developer guide |
|
||||||
|
|
||||||
|
## Permission Check Pattern
|
||||||
|
|
||||||
|
```
|
||||||
|
Request to calendar endpoint
|
||||||
|
↓
|
||||||
|
Check: Is user authenticated?
|
||||||
|
├─ NO → 401 Unauthorized
|
||||||
|
└─ YES ↓
|
||||||
|
Check: Does tenant have can_use_calendar_sync=True?
|
||||||
|
├─ NO → 403 Forbidden (upgrade message)
|
||||||
|
└─ YES ↓
|
||||||
|
Process request
|
||||||
|
├─ Success → 200 OK
|
||||||
|
└─ Error → 500 Server Error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Full Permission Setup
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. Enable feature for tenant
|
||||||
|
from core.models import Tenant
|
||||||
|
tenant = Tenant.objects.get(schema_name='demo')
|
||||||
|
tenant.can_use_calendar_sync = True
|
||||||
|
tenant.save()
|
||||||
|
|
||||||
|
# 2. User tries to access calendar endpoint
|
||||||
|
# GET /api/calendar/list/
|
||||||
|
# → Check: tenant.has_feature('can_use_calendar_sync')
|
||||||
|
# → True! → 200 OK with calendar list
|
||||||
|
|
||||||
|
# 3. Without permission
|
||||||
|
tenant.can_use_calendar_sync = False
|
||||||
|
tenant.save()
|
||||||
|
# GET /api/calendar/list/
|
||||||
|
# → Check: tenant.has_feature('can_use_calendar_sync')
|
||||||
|
# → False! → 403 Forbidden with upgrade message
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- **Full Guide:** `CALENDAR_SYNC_INTEGRATION.md` in smoothschedule/ folder
|
||||||
|
- **Implementation Details:** `CALENDAR_SYNC_PERMISSION_IMPLEMENTATION.md` in project root
|
||||||
|
- **Code:** `schedule/calendar_sync_views.py` (well-commented)
|
||||||
|
- **Tests:** `schedule/tests/test_calendar_sync_permissions.py`
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Check if feature is enabled
|
||||||
|
```python
|
||||||
|
tenant.has_feature('can_use_calendar_sync') # Returns bool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get list of connected calendars
|
||||||
|
```python
|
||||||
|
from core.models import OAuthCredential
|
||||||
|
|
||||||
|
credentials = OAuthCredential.objects.filter(
|
||||||
|
tenant=tenant,
|
||||||
|
purpose='calendar',
|
||||||
|
is_valid=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handle permission denied
|
||||||
|
```python
|
||||||
|
from core.permissions import HasFeaturePermission
|
||||||
|
|
||||||
|
permission = HasFeaturePermission('can_use_calendar_sync')
|
||||||
|
if not permission().has_permission(request, view):
|
||||||
|
# User doesn't have permission
|
||||||
|
# Show upgrade prompt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Feature defaults to **False** for all tenants (opt-in)
|
||||||
|
- Works alongside existing subscription plan system
|
||||||
|
- Follows same pattern as SMS reminders, webhooks, etc.
|
||||||
|
- Multi-tenant isolation built-in
|
||||||
|
- OAuth tokens are encrypted at rest
|
||||||
|
- All operations logged for audit trail
|
||||||
660
README.md
660
README.md
@@ -1,294 +1,470 @@
|
|||||||
# Smooth Schedule - Multi-Tenant SaaS Platform
|
# SmoothSchedule - Multi-Tenant Scheduling Platform
|
||||||
|
|
||||||
A production-grade Django skeleton with **strict data isolation** and **high-trust security** for resource orchestration.
|
A production-ready multi-tenant SaaS platform for resource scheduling, appointments, and business management.
|
||||||
|
|
||||||
## 🎯 Features
|
## Features
|
||||||
|
|
||||||
- ✅ **Multi-Tenancy**: PostgreSQL schema-per-tenant using django-tenants
|
- **Multi-Tenancy**: PostgreSQL schema-per-tenant using django-tenants
|
||||||
- ✅ **8-Tier Role Hierarchy**: From SUPERUSER to CUSTOMER with strict permissions
|
- **8-Tier Role Hierarchy**: SUPERUSER, PLATFORM_MANAGER, PLATFORM_SALES, PLATFORM_SUPPORT, TENANT_OWNER, TENANT_MANAGER, TENANT_STAFF, CUSTOMER
|
||||||
- ✅ **Secure Masquerading**: django-hijack with custom permission matrix
|
- **Modern Stack**: Django 5.2 + React 19 + TypeScript + Vite
|
||||||
- ✅ **Full Audit Trail**: Structured logging of all masquerade activity
|
- **Real-time Updates**: Django Channels + WebSockets
|
||||||
- ✅ **Headless API**: Django Rest Framework (no server-side HTML)
|
- **Background Tasks**: Celery + Redis
|
||||||
- ✅ **Docker Ready**: Complete Docker Compose setup via cookiecutter-django
|
- **Auto SSL**: Let's Encrypt certificates via Traefik
|
||||||
- ✅ **AWS Integration**: S3 storage + Route53 DNS for custom domains
|
- **Cloud Storage**: DigitalOcean Spaces (S3-compatible)
|
||||||
|
- **Docker Ready**: Complete Docker Compose setup for dev and production
|
||||||
|
|
||||||
## 📋 Prerequisites
|
## Project Structure
|
||||||
|
|
||||||
- Python 3.9+
|
```
|
||||||
- PostgreSQL 14+
|
smoothschedule2/
|
||||||
- Docker & Docker Compose
|
├── frontend/ # React + Vite + TypeScript
|
||||||
- Cookiecutter (`pip install cookiecutter`)
|
│ ├── src/
|
||||||
|
│ │ ├── api/ # API client and hooks
|
||||||
|
│ │ ├── components/ # Reusable UI components
|
||||||
|
│ │ ├── hooks/ # React Query hooks
|
||||||
|
│ │ ├── pages/ # Page components
|
||||||
|
│ │ └── types.ts # TypeScript interfaces
|
||||||
|
│ ├── nginx.conf # Production nginx config
|
||||||
|
│ └── Dockerfile.prod # Production frontend container
|
||||||
|
│
|
||||||
|
├── smoothschedule/ # Django backend
|
||||||
|
│ ├── config/ # Django settings
|
||||||
|
│ │ └── settings/
|
||||||
|
│ │ ├── base.py # Base settings
|
||||||
|
│ │ ├── local.py # Local development
|
||||||
|
│ │ └── production.py # Production settings
|
||||||
|
│ ├── smoothschedule/ # Django apps (domain-based)
|
||||||
|
│ │ ├── identity/ # Users, tenants, authentication
|
||||||
|
│ │ │ ├── core/ # Tenant, Domain, middleware
|
||||||
|
│ │ │ └── users/ # User model, MFA, auth
|
||||||
|
│ │ ├── scheduling/ # Core scheduling
|
||||||
|
│ │ │ ├── schedule/ # Resources, Events, Services
|
||||||
|
│ │ │ ├── contracts/ # E-signatures
|
||||||
|
│ │ │ └── analytics/ # Business analytics
|
||||||
|
│ │ ├── communication/ # Notifications, SMS, mobile
|
||||||
|
│ │ ├── commerce/ # Payments, tickets
|
||||||
|
│ │ └── platform/ # Admin, public API
|
||||||
|
│ ├── docker-compose.local.yml
|
||||||
|
│ └── docker-compose.production.yml
|
||||||
|
│
|
||||||
|
├── deploy.sh # Automated deployment script
|
||||||
|
└── CLAUDE.md # Development guide
|
||||||
|
```
|
||||||
|
|
||||||
## 🚀 Quick Start
|
---
|
||||||
|
|
||||||
### 1. Run Setup Script
|
## Local Development Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Docker** and **Docker Compose** (for backend)
|
||||||
|
- **Node.js 22+** and **npm** (for frontend)
|
||||||
|
- **Git**
|
||||||
|
|
||||||
|
### Step 1: Clone the Repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x setup_project.sh
|
git clone https://github.com/your-repo/smoothschedule.git
|
||||||
./setup_project.sh
|
|
||||||
cd smoothschedule
|
cd smoothschedule
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Configure Environment
|
### Step 2: Start the Backend (Django in Docker)
|
||||||
|
|
||||||
Create `.env` file:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Database
|
|
||||||
POSTGRES_DB=smoothschedule_db
|
|
||||||
POSTGRES_USER=smoothschedule_user
|
|
||||||
POSTGRES_PASSWORD=your_secure_password
|
|
||||||
|
|
||||||
# Django
|
|
||||||
DJANGO_SECRET_KEY=your_secret_key_here
|
|
||||||
DJANGO_DEBUG=True
|
|
||||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
|
||||||
|
|
||||||
# AWS
|
|
||||||
AWS_ACCESS_KEY_ID=your_aws_key
|
|
||||||
AWS_SECRET_ACCESS_KEY=your_aws_secret
|
|
||||||
AWS_STORAGE_BUCKET_NAME=smoothschedule-media
|
|
||||||
AWS_ROUTE53_HOSTED_ZONE_ID=your_zone_id
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Start Services
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose build
|
cd smoothschedule
|
||||||
docker-compose up -d
|
|
||||||
|
# Start all backend services
|
||||||
|
docker compose -f docker-compose.local.yml up -d
|
||||||
|
|
||||||
|
# Wait for services to initialize (first time takes longer)
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
# Run database migrations
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py migrate
|
||||||
|
|
||||||
|
# Create a superuser (optional)
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py createsuperuser
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Run Migrations
|
### Step 3: Start the Frontend (React with Vite)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Shared schema
|
cd ../frontend
|
||||||
docker-compose run --rm django python manage.py migrate_schemas --shared
|
|
||||||
|
|
||||||
# Create superuser
|
# Install dependencies
|
||||||
docker-compose run --rm django python manage.py createsuperuser
|
npm install
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Create First Tenant
|
### Step 4: Access the Application
|
||||||
|
|
||||||
|
The application uses `lvh.me` (resolves to 127.0.0.1) for subdomain-based multi-tenancy:
|
||||||
|
|
||||||
|
| URL | Purpose |
|
||||||
|
|-----|---------|
|
||||||
|
| http://platform.lvh.me:5173 | Platform admin dashboard |
|
||||||
|
| http://demo.lvh.me:5173 | Demo tenant (if created) |
|
||||||
|
| http://lvh.me:8000/api/ | Backend API |
|
||||||
|
| http://lvh.me:8000/admin/ | Django admin |
|
||||||
|
|
||||||
|
**Why `lvh.me`?** Browsers don't allow cookies with `domain=.localhost`, but `lvh.me` resolves to 127.0.0.1 and allows proper cookie sharing across subdomains.
|
||||||
|
|
||||||
|
### Local Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend commands (always use docker compose)
|
||||||
|
cd smoothschedule
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose -f docker-compose.local.yml logs -f django
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py migrate
|
||||||
|
|
||||||
|
# Django shell
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py shell
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
docker compose -f docker-compose.local.yml exec django pytest
|
||||||
|
|
||||||
|
# Stop all services
|
||||||
|
docker compose -f docker-compose.local.yml down
|
||||||
|
|
||||||
|
# Frontend commands
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
npm run typecheck
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating a Test Tenant
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd smoothschedule
|
||||||
|
docker compose -f docker-compose.local.yml exec django python manage.py shell
|
||||||
|
```
|
||||||
|
|
||||||
```python
|
```python
|
||||||
docker-compose run --rm django python manage.py shell
|
from smoothschedule.identity.core.models import Tenant, Domain
|
||||||
|
|
||||||
from core.models import Tenant, Domain
|
|
||||||
|
|
||||||
|
# Create tenant
|
||||||
tenant = Tenant.objects.create(
|
tenant = Tenant.objects.create(
|
||||||
name="Demo Company",
|
name="Demo Business",
|
||||||
schema_name="demo",
|
schema_name="demo",
|
||||||
subscription_tier="PROFESSIONAL",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create domain
|
||||||
Domain.objects.create(
|
Domain.objects.create(
|
||||||
domain="demo.smoothschedule.local",
|
domain="demo.lvh.me",
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
is_primary=True,
|
is_primary=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print(f"Created tenant: {tenant.name}")
|
||||||
|
print(f"Access at: http://demo.lvh.me:5173")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Ubuntu/Debian server with Docker and Docker Compose
|
||||||
|
- Domain name with DNS configured:
|
||||||
|
- `A` record: `yourdomain.com` → Server IP
|
||||||
|
- `A` record: `*.yourdomain.com` → Server IP (wildcard)
|
||||||
|
- SSH access to the server
|
||||||
|
|
||||||
|
### Quick Deploy (Existing Server)
|
||||||
|
|
||||||
|
For routine updates to an existing production server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run tenant migrations
|
# From your local machine
|
||||||
docker-compose run --rm django python manage.py migrate_schemas
|
./deploy.sh user@yourdomain.com
|
||||||
|
|
||||||
|
# Or deploy specific services
|
||||||
|
./deploy.sh user@yourdomain.com nginx
|
||||||
|
./deploy.sh user@yourdomain.com django
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🏗️ Architecture
|
### Fresh Server Deployment
|
||||||
|
|
||||||
|
#### Step 1: Server Setup
|
||||||
|
|
||||||
|
SSH into your server and install Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh user@yourdomain.com
|
||||||
|
|
||||||
|
# Install Docker
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sudo sh get-docker.sh
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
|
||||||
|
# Logout and login for group changes
|
||||||
|
exit
|
||||||
|
ssh user@yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: Clone Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~
|
||||||
|
git clone https://github.com/your-repo/smoothschedule.git smoothschedule
|
||||||
|
cd smoothschedule
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Configure Environment Variables
|
||||||
|
|
||||||
|
Create production environment files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p smoothschedule/.envs/.production
|
||||||
|
|
||||||
|
# Django configuration
|
||||||
|
cat > smoothschedule/.envs/.production/.django << 'EOF'
|
||||||
|
DJANGO_SECRET_KEY=your-random-secret-key-here
|
||||||
|
DJANGO_DEBUG=False
|
||||||
|
DJANGO_ALLOWED_HOSTS=yourdomain.com,*.yourdomain.com
|
||||||
|
|
||||||
|
DJANGO_ADMIN_URL=your-secret-admin-path/
|
||||||
|
FRONTEND_URL=https://platform.yourdomain.com
|
||||||
|
PLATFORM_BASE_URL=https://platform.yourdomain.com
|
||||||
|
|
||||||
|
REDIS_URL=redis://redis:6379/0
|
||||||
|
CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
|
|
||||||
|
# DigitalOcean Spaces (or S3)
|
||||||
|
DJANGO_AWS_ACCESS_KEY_ID=your-access-key
|
||||||
|
DJANGO_AWS_SECRET_ACCESS_KEY=your-secret-key
|
||||||
|
DJANGO_AWS_STORAGE_BUCKET_NAME=your-bucket
|
||||||
|
DJANGO_AWS_S3_ENDPOINT_URL=https://nyc3.digitaloceanspaces.com
|
||||||
|
DJANGO_AWS_S3_REGION_NAME=nyc3
|
||||||
|
|
||||||
|
# SSL
|
||||||
|
DJANGO_SECURE_SSL_REDIRECT=True
|
||||||
|
DJANGO_SESSION_COOKIE_SECURE=True
|
||||||
|
DJANGO_CSRF_COOKIE_SECURE=True
|
||||||
|
|
||||||
|
# Cloudflare (for wildcard SSL)
|
||||||
|
CF_DNS_API_TOKEN=your-cloudflare-api-token
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# PostgreSQL configuration
|
||||||
|
cat > smoothschedule/.envs/.production/.postgres << 'EOF'
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_DB=smoothschedule
|
||||||
|
POSTGRES_USER=smoothschedule_user
|
||||||
|
POSTGRES_PASSWORD=your-secure-database-password
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 4: Build and Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# Build all images
|
||||||
|
docker compose -f docker-compose.production.yml build
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
docker compose -f docker-compose.production.yml up -d
|
||||||
|
|
||||||
|
# Wait for startup
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py migrate
|
||||||
|
|
||||||
|
# Collect static files
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
# Create superuser
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 5: Verify Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check all containers are running
|
||||||
|
docker compose -f docker-compose.production.yml ps
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose -f docker-compose.production.yml logs -f
|
||||||
|
|
||||||
|
# Test endpoints
|
||||||
|
curl https://yourdomain.com/api/health/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production URLs
|
||||||
|
|
||||||
|
| URL | Purpose |
|
||||||
|
|-----|---------|
|
||||||
|
| https://yourdomain.com | Marketing site |
|
||||||
|
| https://platform.yourdomain.com | Platform admin |
|
||||||
|
| https://*.yourdomain.com | Tenant subdomains |
|
||||||
|
| https://api.yourdomain.com | API (if configured) |
|
||||||
|
| https://yourdomain.com:5555 | Flower (Celery monitoring) |
|
||||||
|
|
||||||
|
### Production Management Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh user@yourdomain.com
|
||||||
|
cd ~/smoothschedule/smoothschedule
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose -f docker-compose.production.yml logs -f django
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
docker compose -f docker-compose.production.yml restart
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py migrate
|
||||||
|
|
||||||
|
# Django shell
|
||||||
|
docker compose -f docker-compose.production.yml exec django python manage.py shell
|
||||||
|
|
||||||
|
# Database backup
|
||||||
|
docker compose -f docker-compose.production.yml exec postgres pg_dump -U smoothschedule_user smoothschedule > backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
### Multi-Tenancy Model
|
### Multi-Tenancy Model
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────┐
|
PostgreSQL Database
|
||||||
│ PostgreSQL Database │
|
├── public (shared schema)
|
||||||
├─────────────────────────────────────────┤
|
│ ├── Tenants
|
||||||
│ public (shared schema) │
|
│ ├── Domains
|
||||||
│ ├─ Tenants │
|
│ ├── Users
|
||||||
│ ├─ Domains │
|
│ └── PermissionGrants
|
||||||
│ ├─ Users │
|
├── demo (tenant schema)
|
||||||
│ └─ PermissionGrants │
|
│ ├── Resources
|
||||||
├─────────────────────────────────────────┤
|
│ ├── Events
|
||||||
│ tenant_demo (schema for Demo Company) │
|
│ ├── Services
|
||||||
│ ├─ Appointments │
|
│ └── Customers
|
||||||
│ ├─ Resources │
|
└── acme (tenant schema)
|
||||||
│ └─ Customers │
|
├── Resources
|
||||||
├─────────────────────────────────────────┤
|
├── Events
|
||||||
│ tenant_acme (schema for Acme Corp) │
|
└── ...
|
||||||
│ ├─ Appointments │
|
|
||||||
│ ├─ Resources │
|
|
||||||
│ └─ Customers │
|
|
||||||
└─────────────────────────────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Role Hierarchy
|
### Role Hierarchy
|
||||||
|
|
||||||
| Role | Level | Access Scope |
|
| Role | Level | Access |
|
||||||
|---------------------|----------|---------------------------|
|
|------|-------|--------|
|
||||||
| SUPERUSER | Platform | All tenants (god mode) |
|
| SUPERUSER | Platform | All tenants (god mode) |
|
||||||
| PLATFORM_MANAGER | Platform | All tenants |
|
| PLATFORM_MANAGER | Platform | All tenants |
|
||||||
| PLATFORM_SALES | Platform | Demo accounts only |
|
| PLATFORM_SALES | Platform | Demo accounts only |
|
||||||
| PLATFORM_SUPPORT | Platform | Tenant users |
|
| PLATFORM_SUPPORT | Platform | Can masquerade as tenant users |
|
||||||
| TENANT_OWNER | Tenant | Own tenant (full access) |
|
| TENANT_OWNER | Tenant | Full tenant access |
|
||||||
| TENANT_MANAGER | Tenant | Own tenant |
|
| TENANT_MANAGER | Tenant | Most tenant features |
|
||||||
| TENANT_STAFF | Tenant | Own tenant (limited) |
|
| TENANT_STAFF | Tenant | Limited tenant access |
|
||||||
| CUSTOMER | Tenant | Own data only |
|
| CUSTOMER | Tenant | Own data only |
|
||||||
|
|
||||||
### Masquerading Matrix
|
### Request Flow
|
||||||
|
|
||||||
| Hijacker Role | Can Masquerade As |
|
|
||||||
|--------------------|----------------------------------|
|
|
||||||
| SUPERUSER | Anyone |
|
|
||||||
| PLATFORM_SUPPORT | Tenant users |
|
|
||||||
| PLATFORM_SALES | Demo accounts (`is_temporary=True`) |
|
|
||||||
| TENANT_OWNER | Staff in same tenant |
|
|
||||||
| Others | No one |
|
|
||||||
|
|
||||||
**Security Rules:**
|
|
||||||
- Cannot hijack yourself
|
|
||||||
- Cannot hijack SUPERUSERs (except by other SUPERUSERs)
|
|
||||||
- Maximum depth: 1 (no hijack chains)
|
|
||||||
- All attempts logged to `logs/masquerade.log`
|
|
||||||
|
|
||||||
## 📁 Project Structure
|
|
||||||
|
|
||||||
```
|
```
|
||||||
smoothschedule/
|
Browser → Traefik (SSL) → nginx (frontend) or django (API)
|
||||||
├── config/
|
↓
|
||||||
│ └── settings.py # Multi-tenancy & security config
|
React SPA
|
||||||
├── core/
|
↓
|
||||||
│ ├── models.py # Tenant, Domain, PermissionGrant
|
/api/* → django:5000
|
||||||
│ ├── permissions.py # Hijack permission matrix
|
/ws/* → django:5000 (WebSocket)
|
||||||
│ ├── middleware.py # Masquerade audit logging
|
|
||||||
│ └── admin.py # Django admin for core models
|
|
||||||
├── users/
|
|
||||||
│ ├── models.py # Custom User with 8-tier roles
|
|
||||||
│ └── admin.py # User admin with hijack button
|
|
||||||
├── logs/
|
|
||||||
│ ├── security.log # General security events
|
|
||||||
│ └── masquerade.log # Hijack activity (JSON)
|
|
||||||
└── setup_project.sh # Automated setup script
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔐 Security Features
|
|
||||||
|
|
||||||
### Audit Logging
|
|
||||||
|
|
||||||
All masquerade activity is logged in JSON format:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"timestamp": "2024-01-15T10:30:00Z",
|
|
||||||
"action": "HIJACK_START",
|
|
||||||
"hijacker_email": "support@smoothschedule.com",
|
|
||||||
"hijacked_email": "customer@demo.com",
|
|
||||||
"ip_address": "192.168.1.1",
|
|
||||||
"session_key": "abc123..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permission Grants (30-Minute Window)
|
|
||||||
|
|
||||||
Time-limited elevated permissions:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from core.models import PermissionGrant
|
|
||||||
|
|
||||||
grant = PermissionGrant.create_grant(
|
|
||||||
grantor=admin_user,
|
|
||||||
grantee=support_user,
|
|
||||||
action="view_billing",
|
|
||||||
reason="Customer requested billing support",
|
|
||||||
duration_minutes=30,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if active
|
|
||||||
if grant.is_active():
|
|
||||||
# Perform privileged action
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🧪 Testing Masquerading
|
|
||||||
|
|
||||||
1. Access Django Admin: `http://localhost:8000/admin/`
|
|
||||||
2. Create test users with different roles
|
|
||||||
3. Click "Hijack" button next to a user
|
|
||||||
4. Verify audit logs: `docker-compose exec django cat logs/masquerade.log`
|
|
||||||
|
|
||||||
## 📊 Admin Interface
|
|
||||||
|
|
||||||
- **Tenant Management**: View tenants, domains, subscription tiers
|
|
||||||
- **User Management**: Color-coded roles, masquerade buttons
|
|
||||||
- **Permission Grants**: Active/expired/revoked status, bulk revoke
|
|
||||||
- **Domain Verification**: AWS Route53 integration status
|
|
||||||
|
|
||||||
## 🛠️ Development
|
|
||||||
|
|
||||||
### Adding Tenant Apps
|
|
||||||
|
|
||||||
Edit `config/settings.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
TENANT_APPS = [
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'appointments', # Your app
|
|
||||||
'resources', # Your app
|
|
||||||
'billing', # Your app
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Domain Setup
|
|
||||||
|
|
||||||
```python
|
|
||||||
domain = Domain.objects.create(
|
|
||||||
domain="app.customdomain.com",
|
|
||||||
tenant=tenant,
|
|
||||||
is_custom_domain=True,
|
|
||||||
route53_zone_id="Z1234567890ABC",
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Then configure Route53 CNAME: `app.customdomain.com` → `smoothschedule.yourhost.com`
|
|
||||||
|
|
||||||
## 📖 Key Files Reference
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `setup_project.sh` | Automated project initialization |
|
|
||||||
| `config/settings.py` | Multi-tenancy, middleware, security config |
|
|
||||||
| `core/models.py` | Tenant, Domain, PermissionGrant models |
|
|
||||||
| `core/permissions.py` | Masquerading permission matrix |
|
|
||||||
| `core/middleware.py` | Audit logging for masquerading |
|
|
||||||
| `users/models.py` | Custom User with 8-tier roles |
|
|
||||||
|
|
||||||
## 📝 Important Notes
|
|
||||||
|
|
||||||
- **Django Admin**: The ONLY HTML interface (everything else is API)
|
|
||||||
- **Middleware Order**: `TenantMainMiddleware` must be first, `MasqueradeAuditMiddleware` after `HijackUserMiddleware`
|
|
||||||
- **Tenant Isolation**: Each tenant's data is in a separate PostgreSQL schema
|
|
||||||
- **Production**: Update `SECRET_KEY`, database credentials, and AWS keys via environment variables
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
**Cannot create tenant users:**
|
|
||||||
- Error: "Users with role TENANT_STAFF must be assigned to a tenant"
|
|
||||||
- Solution: Set `user.tenant = tenant_instance` before saving
|
|
||||||
|
|
||||||
**Hijack button doesn't appear:**
|
|
||||||
- Check `HIJACK_AUTHORIZATION_CHECK` in settings
|
|
||||||
- Verify `HijackUserAdminMixin` in `users/admin.py`
|
|
||||||
- Ensure user has permission per matrix rules
|
|
||||||
|
|
||||||
**Migrations fail:**
|
|
||||||
- Run shared migrations first: `migrate_schemas --shared`
|
|
||||||
- Then run tenant migrations: `migrate_schemas`
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
This is a production skeleton. Extend `TENANT_APPS` with your business logic.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Built with ❤️ for multi-tenant SaaS perfection**
|
## Configuration Files
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `smoothschedule/docker-compose.local.yml` | Local Docker services |
|
||||||
|
| `smoothschedule/docker-compose.production.yml` | Production Docker services |
|
||||||
|
| `smoothschedule/.envs/.local/` | Local environment variables |
|
||||||
|
| `smoothschedule/.envs/.production/` | Production environment variables |
|
||||||
|
| `smoothschedule/config/settings/` | Django settings |
|
||||||
|
| `smoothschedule/compose/production/traefik/traefik.yml` | Traefik routing config |
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `frontend/.env.development` | Local environment variables |
|
||||||
|
| `frontend/.env.production` | Production environment variables |
|
||||||
|
| `frontend/nginx.conf` | Production nginx config |
|
||||||
|
| `frontend/vite.config.ts` | Vite bundler config |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Backend won't start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Docker logs
|
||||||
|
docker compose -f docker-compose.local.yml logs django
|
||||||
|
|
||||||
|
# Common issues:
|
||||||
|
# - Database not ready: wait longer, then restart django
|
||||||
|
# - Missing migrations: run migrate command
|
||||||
|
# - Port conflict: check if 8000 is in use
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend can't connect to API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify backend is running
|
||||||
|
curl http://lvh.me:8000/api/
|
||||||
|
|
||||||
|
# Check CORS settings in Django
|
||||||
|
# Ensure CORS_ALLOWED_ORIGINS includes http://platform.lvh.me:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSockets disconnecting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check nginx has /ws/ proxy configured
|
||||||
|
# Verify django is running ASGI (Daphne)
|
||||||
|
# Check production traefik/nginx logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-tenant issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check tenant exists
|
||||||
|
docker compose exec django python manage.py shell
|
||||||
|
>>> from smoothschedule.identity.core.models import Tenant, Domain
|
||||||
|
>>> Tenant.objects.all()
|
||||||
|
>>> Domain.objects.all()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Documentation
|
||||||
|
|
||||||
|
- **[CLAUDE.md](CLAUDE.md)** - Development guide, coding standards, architecture details
|
||||||
|
- **[DEPLOYMENT.md](DEPLOYMENT.md)** - Comprehensive deployment guide
|
||||||
|
- **[PRODUCTION_DEPLOYMENT.md](PRODUCTION_DEPLOYMENT.md)** - Step-by-step manual deployment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
4
activepieces-fork/.npmrc
Normal file
4
activepieces-fork/.npmrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@activepieces:registry=https://registry.npmjs.org/
|
||||||
|
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||||
|
legacy-peer-deps=true
|
||||||
|
save-exact=true
|
||||||
56
activepieces-fork/.nx/cache/cloud/2512.16.8/index.js
vendored
Normal file
56
activepieces-fork/.nx/cache/cloud/2512.16.8/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
activepieces-fork/.nx/cache/cloud/2512.16.8/lib/core/commands/fix-ci/utilities-mcp-server.js
vendored
Normal file
6
activepieces-fork/.nx/cache/cloud/2512.16.8/lib/core/commands/fix-ci/utilities-mcp-server.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
activepieces-fork/.nx/cache/cloud/2512.16.8/lib/daemon/process-run-end.js
vendored
Normal file
1
activepieces-fork/.nx/cache/cloud/2512.16.8/lib/daemon/process-run-end.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
activepieces-fork/.nx/cache/cloud/2512.16.8/lib/heartbeat/background-process.js
vendored
Normal file
1
activepieces-fork/.nx/cache/cloud/2512.16.8/lib/heartbeat/background-process.js
vendored
Normal file
File diff suppressed because one or more lines are too long
30
activepieces-fork/.nx/cache/cloud/2512.16.8/package.json
vendored
Normal file
30
activepieces-fork/.nx/cache/cloud/2512.16.8/package.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "nx-cloud-client-bundle",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "commonjs",
|
||||||
|
"main": "index.js",
|
||||||
|
"author": "Victor Savkin",
|
||||||
|
"license": "proprietary",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@opentelemetry/context-async-hooks": "^1.30.1",
|
||||||
|
"@opentelemetry/core": "^1.30.1",
|
||||||
|
"@opentelemetry/resources": "^1.30.1",
|
||||||
|
"@opentelemetry/sdk-trace-base": "^1.30.1",
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.29.0",
|
||||||
|
"axios": "1.1.3",
|
||||||
|
"enquirer": "2.4.1",
|
||||||
|
"dotenv": "~10.0.0",
|
||||||
|
"node-machine-id": "^1.1.12",
|
||||||
|
"tar": "6.1.11",
|
||||||
|
"strip-json-comments": "^5.0.1",
|
||||||
|
"chalk": "4.1.2",
|
||||||
|
"yargs-parser": "21.1.1",
|
||||||
|
"fs-extra": "^11.1.0",
|
||||||
|
"open": "~8.4.0",
|
||||||
|
"html-entities": "^2.6.0",
|
||||||
|
"ini": "4.1.3",
|
||||||
|
"yaml": "2.6.1",
|
||||||
|
"semver": "7.5.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
414
activepieces-fork/.nx/cache/cloud/2512.16.8/static/login-end.html
vendored
Normal file
414
activepieces-fork/.nx/cache/cloud/2512.16.8/static/login-end.html
vendored
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Nx Cloud login</title>
|
||||||
|
<style>
|
||||||
|
*,
|
||||||
|
:before,
|
||||||
|
:after {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
tab-size: 4;
|
||||||
|
font-family:
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
sans-serif,
|
||||||
|
'Apple Color Emoji',
|
||||||
|
'Segoe UI Emoji',
|
||||||
|
Segoe UI Symbol,
|
||||||
|
'Noto Color Emoji';
|
||||||
|
font-feature-settings: normal;
|
||||||
|
font-variation-settings: normal;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
line-height: inherit;
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(51 65 85 / var(--tw-text-opacity));
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
|
/*border-bottom-width: 1px;*/
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(226 232 240 / var(--tw-border-opacity));
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
.mx-auto {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: 5rem;
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.h-14 {
|
||||||
|
height: 3.5rem;
|
||||||
|
}
|
||||||
|
.h-5 {
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
.h-6 {
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
.min-h-full {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
.w-5 {
|
||||||
|
width: 1.25rem;
|
||||||
|
}
|
||||||
|
.w-6 {
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
.w-auto {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.max-w-7xl {
|
||||||
|
max-width: 80rem;
|
||||||
|
}
|
||||||
|
.flex-shrink-0 {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.items-start {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.gap-4 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.gap-8 {
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
.border-b {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
.border-t {
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
.border-slate-100 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(241 245 249 / var(--tw-border-opacity));
|
||||||
|
}
|
||||||
|
.border-slate-200 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(226 232 240 / var(--tw-border-opacity));
|
||||||
|
}
|
||||||
|
.bg-white {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
.p-8 {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
.px-40 {
|
||||||
|
padding-left: 10rem;
|
||||||
|
padding-right: 10rem;
|
||||||
|
}
|
||||||
|
.py-10 {
|
||||||
|
padding-top: 2.5rem;
|
||||||
|
padding-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
.text-2xl {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
.text-wrap {
|
||||||
|
max-width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.leading-7 {
|
||||||
|
line-height: 1.75rem;
|
||||||
|
}
|
||||||
|
.text-slate-400 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
.text-slate-700 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(51 65 85 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
.text-slate-900 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(15 23 42 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
.underline {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
.opacity-50 {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.opacity-25 {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
.transition {
|
||||||
|
transition-property: color, background-color, border-color,
|
||||||
|
text-decoration-color, fill, stroke, opacity, box-shadow, transform,
|
||||||
|
filter, backdrop-filter;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
.hover\:opacity-100:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.sm\:-my-px {
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
.sm\:ml-6 {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
}
|
||||||
|
.sm\:flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.sm\:truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.sm\:px-6 {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
.sm\:text-3xl {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
line-height: 2.25rem;
|
||||||
|
}
|
||||||
|
.sm\:tracking-tight {
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.md\:grid {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.md\:grid-cols-4 {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
.md\:gap-4 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.md\:p-2 {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.lg\:flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.lg\:px-8 {
|
||||||
|
padding-left: 2rem;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="min-h-full">
|
||||||
|
<nav class="border-b border-slate-200 bg-white">
|
||||||
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex h-14 justify-between">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex flex-shrink-0 items-center">
|
||||||
|
<a href="{{ nxCloudUrl }}">
|
||||||
|
<svg
|
||||||
|
id="nx-cloud-header-logo"
|
||||||
|
role="img"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="transparent"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="h-6 w-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-width="2"
|
||||||
|
d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z"
|
||||||
|
id="nx-cloud-header-logo-stroke-1"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke-width="2"
|
||||||
|
d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z"
|
||||||
|
id="nx-cloud-header-logo-stroke-2"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="py-10">
|
||||||
|
<div
|
||||||
|
class="mx-auto flex max-w-7xl flex-col items-start justify-center gap-2 px-40"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="text-2xl leading-7 font-bold text-slate-900 sm:truncate sm:text-3xl sm:tracking-tight"
|
||||||
|
>
|
||||||
|
Your workspace is connected to Nx Cloud
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
<p class="mb-2">You may now close this window.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer
|
||||||
|
class="mt-20 flex border-t border-slate-100 dark:border-slate-800"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
role="menu"
|
||||||
|
aria-labelledby="bottom-navigation"
|
||||||
|
class="mx-auto flex w-auto max-w-7xl items-center px-4 text-slate-400 dark:text-slate-600"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-4 p-8 opacity-50">
|
||||||
|
<a title="Nx Cloud" href="{{ nxCloudUrl }}">
|
||||||
|
<svg
|
||||||
|
id="nx-cloud-header-logo"
|
||||||
|
role="img"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="transparent"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="h-5 w-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-width="2"
|
||||||
|
d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z"
|
||||||
|
id="nx-cloud-header-logo-stroke-1"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke-width="2"
|
||||||
|
d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z"
|
||||||
|
id="nx-cloud-header-logo-stroke-2"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<p class="text-xs transition">© 2024 - Nx Cloud</p>
|
||||||
|
</div>
|
||||||
|
<section
|
||||||
|
class="hidden gap-8 p-8 text-xs opacity-25 transition hover:opacity-100 md:grid md:grid-cols-4 md:gap-4 md:p-2 lg:flex"
|
||||||
|
>
|
||||||
|
<a href="https://nx.app/terms" title="Terms of Service"
|
||||||
|
>Terms of Service</a
|
||||||
|
><a href="https://nx.app/privacy" title="Privacy Policy"
|
||||||
|
>Privacy Policy</a
|
||||||
|
><a
|
||||||
|
href="https://status.nx.app"
|
||||||
|
title="Status"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>Status</a
|
||||||
|
><a
|
||||||
|
href="https://nx.dev/ci/intro/ci-with-nx?utm_source=nx.app"
|
||||||
|
title="Docs"
|
||||||
|
>Docs</a
|
||||||
|
><a href="mailto:cloud-support@nrwl.io" title="Contact Nx Cloud"
|
||||||
|
>Contact Nx Cloud</a
|
||||||
|
><a href="https://nx.dev/pricing?utm_source=nx.app" title="Pricing"
|
||||||
|
>Pricing</a
|
||||||
|
><a href="https://nx.dev/company?utm_source=nx.app" title="Company"
|
||||||
|
>Company</a
|
||||||
|
><a
|
||||||
|
href="https://twitter.com/nxdevtools"
|
||||||
|
title="@NxDevTools"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>@NxDevTools</a
|
||||||
|
>
|
||||||
|
</section>
|
||||||
|
</nav>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
432
activepieces-fork/.nx/cache/cloud/2512.16.8/static/login-start.html
vendored
Normal file
432
activepieces-fork/.nx/cache/cloud/2512.16.8/static/login-start.html
vendored
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Nx Cloud login</title>
|
||||||
|
<style>
|
||||||
|
*,
|
||||||
|
:before,
|
||||||
|
:after {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
tab-size: 4;
|
||||||
|
font-family:
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
sans-serif,
|
||||||
|
'Apple Color Emoji',
|
||||||
|
'Segoe UI Emoji',
|
||||||
|
Segoe UI Symbol,
|
||||||
|
'Noto Color Emoji';
|
||||||
|
font-feature-settings: normal;
|
||||||
|
font-variation-settings: normal;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
line-height: inherit;
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(51 65 85 / var(--tw-text-opacity));
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
|
/*border-bottom-width: 1px;*/
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(226 232 240 / var(--tw-border-opacity));
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
.mx-auto {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: 5rem;
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.h-14 {
|
||||||
|
height: 3.5rem;
|
||||||
|
}
|
||||||
|
.h-5 {
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
.h-6 {
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
.min-h-full {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
.w-5 {
|
||||||
|
width: 1.25rem;
|
||||||
|
}
|
||||||
|
.w-6 {
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
.w-auto {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.max-w-7xl {
|
||||||
|
max-width: 80rem;
|
||||||
|
}
|
||||||
|
.flex-shrink-0 {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.items-start {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.gap-4 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.gap-8 {
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
.border-b {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
.border-t {
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
.border-slate-100 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(241 245 249 / var(--tw-border-opacity));
|
||||||
|
}
|
||||||
|
.border-slate-200 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(226 232 240 / var(--tw-border-opacity));
|
||||||
|
}
|
||||||
|
.bg-white {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
.p-8 {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
.px-40 {
|
||||||
|
padding-left: 10rem;
|
||||||
|
padding-right: 10rem;
|
||||||
|
}
|
||||||
|
.py-10 {
|
||||||
|
padding-top: 2.5rem;
|
||||||
|
padding-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
.text-2xl {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.leading-7 {
|
||||||
|
line-height: 1.75rem;
|
||||||
|
}
|
||||||
|
.text-slate-400 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
.text-slate-700 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(51 65 85 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
.text-slate-900 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(15 23 42 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
.underline {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
.opacity-50 {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.opacity-25 {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
.transition {
|
||||||
|
transition-property: color, background-color, border-color,
|
||||||
|
text-decoration-color, fill, stroke, opacity, box-shadow, transform,
|
||||||
|
filter, backdrop-filter;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
.hover\:opacity-100:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.sm\:-my-px {
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
.sm\:ml-6 {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
}
|
||||||
|
.sm\:flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.sm\:truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.sm\:px-6 {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
.sm\:text-3xl {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
line-height: 2.25rem;
|
||||||
|
}
|
||||||
|
.sm\:tracking-tight {
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.md\:grid {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.md\:grid-cols-4 {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
.md\:gap-4 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.md\:p-2 {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.lg\:flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.lg\:px-8 {
|
||||||
|
padding-left: 2rem;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
function startCountdown() {
|
||||||
|
let countdownElement = document.getElementById('countdown');
|
||||||
|
let countdown = 5;
|
||||||
|
|
||||||
|
let interval = setInterval(function () {
|
||||||
|
countdownElement.innerHTML = countdown;
|
||||||
|
countdown--;
|
||||||
|
|
||||||
|
if (countdown < 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
window.location.href = '{{ redirectUrl }}'; // Replace with your redirect URL
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onload="startCountdown()">
|
||||||
|
<div class="min-h-full">
|
||||||
|
<nav class="border-b border-slate-200 bg-white">
|
||||||
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex h-14 justify-between">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex flex-shrink-0 items-center">
|
||||||
|
<a href="{{ nxCloudUrl }}">
|
||||||
|
<svg
|
||||||
|
id="nx-cloud-header-logo"
|
||||||
|
role="img"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="transparent"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="h-6 w-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-width="2"
|
||||||
|
d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z"
|
||||||
|
id="nx-cloud-header-logo-stroke-1"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke-width="2"
|
||||||
|
d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z"
|
||||||
|
id="nx-cloud-header-logo-stroke-2"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="py-10">
|
||||||
|
<div
|
||||||
|
class="mx-auto flex max-w-7xl flex-col items-start justify-center gap-2 px-40"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="text-2xl leading-7 font-bold text-slate-900 sm:truncate sm:text-3xl sm:tracking-tight"
|
||||||
|
>
|
||||||
|
Nx Cloud Login
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
<p class="mb-2">
|
||||||
|
You will be redirected to
|
||||||
|
<a class="font-semibold underline" href="{{ redirectUrl }}"
|
||||||
|
>Nx Cloud</a
|
||||||
|
>
|
||||||
|
in <span id="countdown">5</span> seconds.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer
|
||||||
|
class="mt-20 flex border-t border-slate-100 dark:border-slate-800"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
role="menu"
|
||||||
|
aria-labelledby="bottom-navigation"
|
||||||
|
class="mx-auto flex w-auto max-w-7xl items-center px-4 text-slate-400 dark:text-slate-600"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-4 p-8 opacity-50">
|
||||||
|
<a title="Nx Cloud" href="{{ nxCloudUrl }}">
|
||||||
|
<svg
|
||||||
|
id="nx-cloud-header-logo"
|
||||||
|
role="img"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="transparent"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="h-5 w-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-width="2"
|
||||||
|
d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z"
|
||||||
|
id="nx-cloud-header-logo-stroke-1"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke-width="2"
|
||||||
|
d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z"
|
||||||
|
id="nx-cloud-header-logo-stroke-2"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<p class="text-xs transition">© 2024 - Nx Cloud</p>
|
||||||
|
</div>
|
||||||
|
<section
|
||||||
|
class="hidden gap-8 p-8 text-xs opacity-25 transition hover:opacity-100 md:grid md:grid-cols-4 md:gap-4 md:p-2 lg:flex"
|
||||||
|
>
|
||||||
|
<a href="https://nx.app/terms" title="Terms of Service"
|
||||||
|
>Terms of Service</a
|
||||||
|
><a href="https://nx.app/privacy" title="Privacy Policy"
|
||||||
|
>Privacy Policy</a
|
||||||
|
><a
|
||||||
|
href="https://status.nx.app"
|
||||||
|
title="Status"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>Status</a
|
||||||
|
><a
|
||||||
|
href="https://nx.dev/ci/intro/ci-with-nx?utm_source=nx.app"
|
||||||
|
title="Docs"
|
||||||
|
>Docs</a
|
||||||
|
><a href="mailto:cloud-support@nrwl.io" title="Contact Nx Cloud"
|
||||||
|
>Contact Nx Cloud</a
|
||||||
|
><a href="https://nx.dev/pricing?utm_source=nx.app" title="Pricing"
|
||||||
|
>Pricing</a
|
||||||
|
><a href="https://nx.dev/company?utm_source=nx.app" title="Company"
|
||||||
|
>Company</a
|
||||||
|
><a
|
||||||
|
href="https://twitter.com/nxdevtools"
|
||||||
|
title="@NxDevTools"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>@NxDevTools</a
|
||||||
|
>
|
||||||
|
</section>
|
||||||
|
</nav>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
activepieces-fork/.nx/cache/cloud/verify.lock
vendored
Normal file
1
activepieces-fork/.nx/cache/cloud/verify.lock
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1766103708902
|
||||||
13
activepieces-fork/CONTRIBUTING.md
Normal file
13
activepieces-fork/CONTRIBUTING.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!-- omit in toc -->
|
||||||
|
# Contributing to Activepieces
|
||||||
|
|
||||||
|
First off, thanks for taking the time to contribute! ❤️
|
||||||
|
|
||||||
|
All types of contributions are encouraged and valued. See the [Contributing Guide](https://www.activepieces.com/docs/developers/building-pieces/start-building) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
|
||||||
|
|
||||||
|
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
||||||
|
> - Star the project
|
||||||
|
> - Tweet about it
|
||||||
|
> - Refer this project in your project's readme
|
||||||
|
> - Mention the project at local meetups and tell your friends/colleagues
|
||||||
|
|
||||||
123
activepieces-fork/Dockerfile
Normal file
123
activepieces-fork/Dockerfile
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
FROM node:20.19-bullseye-slim AS base
|
||||||
|
|
||||||
|
# Set environment variables early for better layer caching
|
||||||
|
ENV LANG=en_US.UTF-8 \
|
||||||
|
LANGUAGE=en_US:en \
|
||||||
|
LC_ALL=en_US.UTF-8 \
|
||||||
|
NX_DAEMON=false \
|
||||||
|
NX_NO_CLOUD=true
|
||||||
|
|
||||||
|
# Install all system dependencies in a single layer with cache mounts
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
openssh-client \
|
||||||
|
python3 \
|
||||||
|
g++ \
|
||||||
|
build-essential \
|
||||||
|
git \
|
||||||
|
poppler-utils \
|
||||||
|
poppler-data \
|
||||||
|
procps \
|
||||||
|
locales \
|
||||||
|
locales-all \
|
||||||
|
unzip \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
libcap-dev && \
|
||||||
|
yarn config set python /usr/bin/python3
|
||||||
|
|
||||||
|
RUN export ARCH=$(uname -m) && \
|
||||||
|
if [ "$ARCH" = "x86_64" ]; then \
|
||||||
|
curl -fSL https://github.com/oven-sh/bun/releases/download/bun-v1.3.1/bun-linux-x64-baseline.zip -o bun.zip; \
|
||||||
|
elif [ "$ARCH" = "aarch64" ]; then \
|
||||||
|
curl -fSL https://github.com/oven-sh/bun/releases/download/bun-v1.3.1/bun-linux-aarch64.zip -o bun.zip; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN unzip bun.zip \
|
||||||
|
&& mv bun-*/bun /usr/local/bin/bun \
|
||||||
|
&& chmod +x /usr/local/bin/bun \
|
||||||
|
&& rm -rf bun.zip bun-*
|
||||||
|
|
||||||
|
RUN bun --version
|
||||||
|
|
||||||
|
# Install global npm packages in a single layer
|
||||||
|
RUN --mount=type=cache,target=/root/.npm \
|
||||||
|
npm install -g --no-fund --no-audit \
|
||||||
|
node-gyp \
|
||||||
|
npm@9.9.3 \
|
||||||
|
pm2@6.0.10 \
|
||||||
|
typescript@4.9.4
|
||||||
|
|
||||||
|
# Install isolated-vm globally (needed for sandboxes)
|
||||||
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||||
|
cd /usr/src && bun install isolated-vm@5.0.1
|
||||||
|
|
||||||
|
### STAGE 1: Build ###
|
||||||
|
FROM base AS build
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Copy only dependency files first for better layer caching
|
||||||
|
COPY .npmrc package.json bun.lock ./
|
||||||
|
|
||||||
|
# Install all dependencies with frozen lockfile
|
||||||
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source code after dependency installation
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build all projects including the SmoothSchedule piece
|
||||||
|
RUN npx nx run-many --target=build --projects=react-ui,server-api,pieces-smoothschedule --configuration production --parallel=2 --skip-nx-cache
|
||||||
|
|
||||||
|
# Install production dependencies only for the backend API
|
||||||
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||||
|
cd dist/packages/server/api && \
|
||||||
|
bun install --production --frozen-lockfile
|
||||||
|
|
||||||
|
# Install dependencies for the SmoothSchedule piece
|
||||||
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||||
|
cd dist/packages/pieces/community/smoothschedule && \
|
||||||
|
bun install --production
|
||||||
|
|
||||||
|
### STAGE 2: Run ###
|
||||||
|
FROM base AS run
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Install Nginx and gettext in a single layer with cache mount
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends nginx gettext
|
||||||
|
|
||||||
|
# Copy static configuration files first (better layer caching)
|
||||||
|
COPY nginx.react.conf /etc/nginx/nginx.conf
|
||||||
|
COPY --from=build /usr/src/app/packages/server/api/src/assets/default.cf /usr/local/etc/isolate
|
||||||
|
COPY docker-entrypoint.sh .
|
||||||
|
|
||||||
|
# Create all necessary directories in one layer
|
||||||
|
RUN mkdir -p \
|
||||||
|
/usr/src/app/dist/packages/server \
|
||||||
|
/usr/src/app/dist/packages/engine \
|
||||||
|
/usr/src/app/dist/packages/shared \
|
||||||
|
/usr/src/app/dist/packages/pieces && \
|
||||||
|
chmod +x docker-entrypoint.sh
|
||||||
|
|
||||||
|
# Copy built artifacts from build stage
|
||||||
|
COPY --from=build /usr/src/app/LICENSE .
|
||||||
|
COPY --from=build /usr/src/app/dist/packages/engine/ ./dist/packages/engine/
|
||||||
|
COPY --from=build /usr/src/app/dist/packages/server/ ./dist/packages/server/
|
||||||
|
COPY --from=build /usr/src/app/dist/packages/shared/ ./dist/packages/shared/
|
||||||
|
COPY --from=build /usr/src/app/dist/packages/pieces/ ./dist/packages/pieces/
|
||||||
|
COPY --from=build /usr/src/app/packages ./packages
|
||||||
|
|
||||||
|
# Copy frontend files to Nginx document root
|
||||||
|
COPY --from=build /usr/src/app/dist/packages/react-ui /usr/share/nginx/html/
|
||||||
|
|
||||||
|
LABEL service=activepieces
|
||||||
|
|
||||||
|
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||||
|
EXPOSE 80
|
||||||
25
activepieces-fork/LICENSE
Executable file
25
activepieces-fork/LICENSE
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
Copyright (c) 2020-2024 Activepieces Inc.
|
||||||
|
|
||||||
|
Portions of this software are licensed as follows:
|
||||||
|
|
||||||
|
* All content that resides under the "packages/ee/" and "packages/server/api/src/app/ee" directory of this repository, if that directory exists, is licensed under the license defined in packages/ee/LICENSE
|
||||||
|
* All third party components incorporated into the Activepieces Inc Software are licensed under the original license provided by the owner of the applicable component.
|
||||||
|
* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
464
activepieces-fork/README.md
Normal file
464
activepieces-fork/README.md
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
|
||||||
|
<h1 align="center">
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://activepieces.com"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
align="center"
|
||||||
|
alt="Activepieces"
|
||||||
|
src="https://github.com/activepieces/activepieces/assets/1812998/76c97441-c285-4480-bc75-30a0c73ed340"
|
||||||
|
style="width:100%;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="/LICENSE" target="_blank"><img src='https://img.shields.io/badge/license-MIT-green?style=for-the-badge' /></a> <img src='https://img.shields.io/github/commit-activity/w/activepieces/activepieces/main?style=for-the-badge' /> <a href='https://discord.gg/2jUXBKDdP8'><img src='https://img.shields.io/discord/966798490984382485?style=for-the-badge' /></a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
An open source replacement for Zapier
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a
|
||||||
|
href="https://www.activepieces.com/docs"
|
||||||
|
target="_blank"
|
||||||
|
><b>Documentation</b></a> 🌪️
|
||||||
|
<a
|
||||||
|
href="https://www.activepieces.com/docs/developers/building-pieces/overview"
|
||||||
|
target="_blank"
|
||||||
|
><b>Create a Piece</b></a> 🖉
|
||||||
|
<a
|
||||||
|
href="https://www.activepieces.com/docs/install/overview"
|
||||||
|
target="_blank"
|
||||||
|
><b>Deploy</b></a> 🔥
|
||||||
|
<a
|
||||||
|
href="https://discord.gg/yvxF5k5AUb"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<b>Join Discord</b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
# 🤯 Welcome to Activepieces
|
||||||
|
|
||||||
|
All-in-one AI automation designed to be **extensible** through a **type-safe** pieces framework written in **TypeScript**.
|
||||||
|
When you contribute pieces to Activepieces they become automatically available as MCP servers that you can use with LLMs through Claude Desktop, Cursor or Windsurf!
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 🔥 Why Activepieces is Different:
|
||||||
|
|
||||||
|
- **💖 Loved by Everyone**: Intuitive interface and great experience for both technical and non-technical users with a quick learning curve.
|
||||||
|
|
||||||
|
<img src="/docs/resources/templates.gif">
|
||||||
|
|
||||||
|
- **🌐 Open Ecosystem:** All pieces are open source and available on npmjs.com, **60% of the pieces are contributed by the community**.
|
||||||
|
|
||||||
|
- **🛠️ Largest open source MCP toolkit**: All our pieces (280+) are available as MCP that you can use with LLMs on Claude Desktop, Cursor or Windsurf.
|
||||||
|
|
||||||
|
- **🛠️ Pieces are written in Typescript**: Pieces are npm packages in TypeScript, offering full customization with the best developer experience, including **hot reloading** for **local** piece development on your machine. 😎
|
||||||
|
|
||||||
|
<img src="/docs/resources/create-action.png" alt="">
|
||||||
|
|
||||||
|
- **🤖 AI-First**: Native AI pieces let you experiment with various providers, or create your own agents using our AI SDK to help you build flows inside the builder.
|
||||||
|
|
||||||
|
- **🏢 Enterprise-Ready**: Developers set up the tools, and anyone in the organization can use the no-code builder. Full customization from branding to control.
|
||||||
|
|
||||||
|
- **🔒 Secure by Design**: Self-hosted and network-gapped for maximum security and control over your data.
|
||||||
|
|
||||||
|
- **🧠 Human in the Loop**: Delay execution for a period of time or require approval. These are just pieces built on top of the piece framework, and you can build many pieces like that. 🎨
|
||||||
|
|
||||||
|
- **💻 Human Input Interfaces**: Built-in support for human input triggers like "Chat Interface" 💬 and "Form Interface" 📝
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🛠️ Builder Features:
|
||||||
|
|
||||||
|
- [x] Loops
|
||||||
|
- [x] Branches
|
||||||
|
- [x] Auto Retries
|
||||||
|
- [x] HTTP
|
||||||
|
- [x] Code with **NPM**
|
||||||
|
- [x] ASK AI in Code Piece (Non technical user can clean data without knowing to code)
|
||||||
|
- [x] Flows are fully versioned.
|
||||||
|
- [x] Languages Translations
|
||||||
|
- [x] Customizable Templates
|
||||||
|
- [X] 200+ Pieces, check https://www.activepieces.com/pieces
|
||||||
|
|
||||||
|
**We release updates frequently. Check the product changelog for the latest features.**
|
||||||
|
|
||||||
|
|
||||||
|
## 🔌 Create Your Own Piece
|
||||||
|
|
||||||
|
Activepieces supports integrations with Google Sheets, OpenAI, Discord, RSS, and over 200 other services. [Check out the full list of supported integrations](https://www.activepieces.com/pieces), which is constantly expanding thanks to our community's contributions.
|
||||||
|
|
||||||
|
As an **open ecosystem**, all integration source code is accessible in our repository. These integrations are versioned and [published](https://www.npmjs.com/search?q=%40activepieces) directly to npmjs.com upon contribution.
|
||||||
|
|
||||||
|
You can easily create your own integration using our TypeScript framework. For detailed instructions, please refer to our [Contributor's Guide](https://www.activepieces.com/docs/developers/building-pieces/overview).
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
Activepieces' Community Edition is released as open source under the [MIT license](https://github.com/activepieces/activepieces/blob/main/LICENSE) and enterprise features are released under [Commercial License](https://github.com/activepieces/activepieces/blob/main/packages/ee/LICENSE)
|
||||||
|
|
||||||
|
|
||||||
|
Read more about the feature comparison here https://www.activepieces.com/docs/about/editions
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
|
||||||
|
# 💭 Join Our Community
|
||||||
|
|
||||||
|
<a href="https://discord.gg/2jUXBKDdP8" target="_blank">
|
||||||
|
<img src="https://discordapp.com/api/guilds/966798490984382485/widget.png?style=banner3" alt="">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
# 🌐 Contributions
|
||||||
|
|
||||||
|
We welcome contributions big or small and in different directions. The best way to do this is to check this [document](https://www.activepieces.com/docs/developers/building-pieces/create-action) and we are always up to talk on [our Discord Server](https://discord.gg/2jUXBKDdP8).
|
||||||
|
|
||||||
|
## 📚 Translations
|
||||||
|
|
||||||
|
Not into coding but still interested in contributing? Come join our [Discord](https://discord.gg/2jUXBKDdP8) and visit https://www.activepieces.com/docs/about/i18n for more information.
|
||||||
|
|
||||||
|
].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-16093902-626364-update.json)
|
||||||
|
|
||||||
|
].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-16093902-626364-update.json)
|
||||||
|
|
||||||
|
].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-16093902-626364-update.json)
|
||||||
|
|
||||||
|
].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-16093902-626364-update.json)
|
||||||
|
|
||||||
|
|
||||||
|
].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-16093902-626364-update.json)
|
||||||
|
|
||||||
|
|
||||||
|
## 🦫 Contributors
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShahedAlMashni"><img src="https://avatars.githubusercontent.com/u/41443850?v=4?s=100" width="100px;" alt="ShahedAlMashni"/><br /><sub><b>ShahedAlMashni</b></sub></a><br /><a href="#plugin-ShahedAlMashni" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AbdulTheActivePiecer"><img src="https://avatars.githubusercontent.com/u/106555838?v=4?s=100" width="100px;" alt="AbdulTheActivePiecer"/><br /><sub><b>AbdulTheActivePiecer</b></sub></a><br /><a href="#maintenance-AbdulTheActivePiecer" title="Maintenance">🚧</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/khaledmashaly"><img src="https://avatars.githubusercontent.com/u/61781545?v=4?s=100" width="100px;" alt="Khaled Mashaly"/><br /><sub><b>Khaled Mashaly</b></sub></a><br /><a href="#maintenance-khaledmashaly" title="Maintenance">🚧</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abuaboud"><img src="https://avatars.githubusercontent.com/u/1812998?v=4?s=100" width="100px;" alt="Mohammed Abu Aboud"/><br /><sub><b>Mohammed Abu Aboud</b></sub></a><br /><a href="#maintenance-abuaboud" title="Maintenance">🚧</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://aboudzein.github.io"><img src="https://avatars.githubusercontent.com/u/12976630?v=4?s=100" width="100px;" alt="Abdulrahman Zeineddin"/><br /><sub><b>Abdulrahman Zeineddin</b></sub></a><br /><a href="#plugin-aboudzein" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/creed983"><img src="https://avatars.githubusercontent.com/u/62152944?v=4?s=100" width="100px;" alt="ahmad jaber"/><br /><sub><b>ahmad jaber</b></sub></a><br /><a href="#plugin-creed983" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ashrafsamhouri"><img src="https://avatars.githubusercontent.com/u/97393596?v=4?s=100" width="100px;" alt="ashrafsamhouri"/><br /><sub><b>ashrafsamhouri</b></sub></a><br /><a href="#plugin-ashrafsamhouri" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://steercampaign.com"><img src="https://avatars.githubusercontent.com/u/12627658?v=4?s=100" width="100px;" alt="Mohammad Abu Musa"/><br /><sub><b>Mohammad Abu Musa</b></sub></a><br /><a href="#projectManagement-mabumusa1" title="Project Management">📆</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kanarelo"><img src="https://avatars.githubusercontent.com/u/393261?v=4?s=100" width="100px;" alt="Mukewa Wekalao"/><br /><sub><b>Mukewa Wekalao</b></sub></a><br /><a href="#plugin-kanarelo" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://osamahaikal.me/"><img src="https://avatars.githubusercontent.com/u/72370395?v=4?s=100" width="100px;" alt="Osama Abdallah Essa Haikal"/><br /><sub><b>Osama Abdallah Essa Haikal</b></sub></a><br /><a href="#plugin-OsamaHaikal" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/M-Arman"><img src="https://avatars.githubusercontent.com/u/54455592?v=4?s=100" width="100px;" alt="Arman"/><br /><sub><b>Arman</b></sub></a><br /><a href="#security-M-Arman" title="Security">🛡️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oskarkraemer"><img src="https://avatars.githubusercontent.com/u/42745862?v=4?s=100" width="100px;" alt="Oskar Krämer"/><br /><sub><b>Oskar Krämer</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=oskarkraemer" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://thibpat.com"><img src="https://avatars.githubusercontent.com/u/494686?v=4?s=100" width="100px;" alt="Thibaut Patel"/><br /><sub><b>Thibaut Patel</b></sub></a><br /><a href="#ideas-tpatel" title="Ideas, Planning, & Feedback">🤔</a> <a href="#plugin-tpatel" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Applesaucesomer"><img src="https://avatars.githubusercontent.com/u/18318905?v=4?s=100" width="100px;" alt="Applesaucesomer"/><br /><sub><b>Applesaucesomer</b></sub></a><br /><a href="#ideas-Applesaucesomer" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/crazyTweek"><img src="https://avatars.githubusercontent.com/u/6828237?v=4?s=100" width="100px;" alt="crazyTweek"/><br /><sub><b>crazyTweek</b></sub></a><br /><a href="#ideas-crazyTweek" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://linkedin.com/in/muhammad-tabaza"><img src="https://avatars.githubusercontent.com/u/23503983?v=4?s=100" width="100px;" alt="Muhammad Tabaza"/><br /><sub><b>Muhammad Tabaza</b></sub></a><br /><a href="#plugin-m-tabaza" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://shaypunter.co.uk"><img src="https://avatars.githubusercontent.com/u/18310437?v=4?s=100" width="100px;" alt="Shay Punter"/><br /><sub><b>Shay Punter</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=ShayPunter" title="Documentation">📖</a> <a href="#plugin-ShayPunter" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abaza738"><img src="https://avatars.githubusercontent.com/u/50132270?v=4?s=100" width="100px;" alt="abaza738"/><br /><sub><b>abaza738</b></sub></a><br /><a href="#plugin-abaza738" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jonaboe"><img src="https://avatars.githubusercontent.com/u/51358680?v=4?s=100" width="100px;" alt="Jona Boeddinghaus"/><br /><sub><b>Jona Boeddinghaus</b></sub></a><br /><a href="#plugin-jonaboe" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fomojola"><img src="https://avatars.githubusercontent.com/u/264253?v=4?s=100" width="100px;" alt="fomojola"/><br /><sub><b>fomojola</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=fomojola" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/astorozhevsky"><img src="https://avatars.githubusercontent.com/u/11055414?v=4?s=100" width="100px;" alt="Alexander Storozhevsky"/><br /><sub><b>Alexander Storozhevsky</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=astorozhevsky" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/J0LGER"><img src="https://avatars.githubusercontent.com/u/54769522?v=4?s=100" width="100px;" alt="J0LGER"/><br /><sub><b>J0LGER</b></sub></a><br /><a href="#security-J0LGER" title="Security">🛡️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://about.me/veverkap"><img src="https://avatars.githubusercontent.com/u/22348?v=4?s=100" width="100px;" alt="Patrick Veverka"/><br /><sub><b>Patrick Veverka</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Aveverkap" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://berksmbl.com"><img src="https://avatars.githubusercontent.com/u/10000339?v=4?s=100" width="100px;" alt="Berk Sümbül"/><br /><sub><b>Berk Sümbül</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=berksmbl" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Willianwg"><img src="https://avatars.githubusercontent.com/u/51550522?v=4?s=100" width="100px;" alt="Willian Guedes"/><br /><sub><b>Willian Guedes</b></sub></a><br /><a href="#plugin-Willianwg" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abdullahranginwala"><img src="https://avatars.githubusercontent.com/u/19731056?v=4?s=100" width="100px;" alt="Abdullah Ranginwala"/><br /><sub><b>Abdullah Ranginwala</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=abdullahranginwala" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dentych"><img src="https://avatars.githubusercontent.com/u/2256372?v=4?s=100" width="100px;" alt="Dennis Tychsen"/><br /><sub><b>Dennis Tychsen</b></sub></a><br /><a href="#plugin-dentych" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MyWay"><img src="https://avatars.githubusercontent.com/u/1765284?v=4?s=100" width="100px;" alt="MyWay"/><br /><sub><b>MyWay</b></sub></a><br /><a href="#plugin-MyWay" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bibhuty-did-this"><img src="https://avatars.githubusercontent.com/u/28416188?v=4?s=100" width="100px;" alt="Bibhuti Bhusan Panda"/><br /><sub><b>Bibhuti Bhusan Panda</b></sub></a><br /><a href="#plugin-bibhuty-did-this" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tarunsamanta2k20"><img src="https://avatars.githubusercontent.com/u/55488549?v=4?s=100" width="100px;" alt="Tarun Samanta"/><br /><sub><b>Tarun Samanta</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Atarunsamanta2k20" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/herman-kudria-10868b207/"><img src="https://avatars.githubusercontent.com/u/9007211?v=4?s=100" width="100px;" alt="Herman Kudria"/><br /><sub><b>Herman Kudria</b></sub></a><br /><a href="#plugin-HKudria" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://nulldev.imagefoo.com/"><img src="https://avatars.githubusercontent.com/u/66683380?v=4?s=100" width="100px;" alt="[NULL] Dev"/><br /><sub><b>[NULL] Dev</b></sub></a><br /><a href="#plugin-Abdallah-Alwarawreh" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JanHolger"><img src="https://avatars.githubusercontent.com/u/25184957?v=4?s=100" width="100px;" alt="Jan Bebendorf"/><br /><sub><b>Jan Bebendorf</b></sub></a><br /><a href="#plugin-JanHolger" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.nileshtrivedi.com"><img src="https://avatars.githubusercontent.com/u/19304?v=4?s=100" width="100px;" alt="Nilesh"/><br /><sub><b>Nilesh</b></sub></a><br /><a href="#plugin-nileshtrivedi" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://certopus.com"><img src="https://avatars.githubusercontent.com/u/40790016?v=4?s=100" width="100px;" alt="Vraj Gohil"/><br /><sub><b>Vraj Gohil</b></sub></a><br /><a href="#plugin-VrajGohil" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BastienMe"><img src="https://avatars.githubusercontent.com/u/71411115?v=4?s=100" width="100px;" alt="BastienMe"/><br /><sub><b>BastienMe</b></sub></a><br /><a href="#plugin-BastienMe" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://blog.fosketts.net"><img src="https://avatars.githubusercontent.com/u/8627862?v=4?s=100" width="100px;" alt="Stephen Foskett"/><br /><sub><b>Stephen Foskett</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=SFoskett" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://ganapati.fr"><img src="https://avatars.githubusercontent.com/u/15729117?v=4?s=100" width="100px;" alt="Nathan"/><br /><sub><b>Nathan</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=asuri0n" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.n-soft.pl"><img src="https://avatars.githubusercontent.com/u/4056319?v=4?s=100" width="100px;" alt="Marcin Natanek"/><br /><sub><b>Marcin Natanek</b></sub></a><br /><a href="#plugin-mnatanek" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://all-tech-plus.com"><img src="https://avatars.githubusercontent.com/u/23551912?v=4?s=100" width="100px;" alt="Mark van Bellen"/><br /><sub><b>Mark van Bellen</b></sub></a><br /><a href="#plugin-buttonsbond" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://guzguz.fr"><img src="https://avatars.githubusercontent.com/u/13715916?v=4?s=100" width="100px;" alt="Olivier Guzzi"/><br /><sub><b>Olivier Guzzi</b></sub></a><br /><a href="#plugin-olivierguzzi" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ozak93"><img src="https://avatars.githubusercontent.com/u/31257994?v=4?s=100" width="100px;" alt="Osama Zakarneh"/><br /><sub><b>Osama Zakarneh</b></sub></a><br /><a href="#plugin-Ozak93" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/phestvik"><img src="https://avatars.githubusercontent.com/u/88210985?v=4?s=100" width="100px;" alt="phestvik"/><br /><sub><b>phestvik</b></sub></a><br /><a href="#ideas-phestvik" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://website-portfolio-bucket.s3-website-ap-northeast-1.amazonaws.com/"><img src="https://avatars.githubusercontent.com/u/113296626?v=4?s=100" width="100px;" alt="Rajdeep Pal"/><br /><sub><b>Rajdeep Pal</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=Rajdeep1311" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.tepote.com"><img src="https://avatars.githubusercontent.com/u/40870?v=4?s=100" width="100px;" alt="Camilo Usuga"/><br /><sub><b>Camilo Usuga</b></sub></a><br /><a href="#plugin-camilou" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kishanprmr"><img src="https://avatars.githubusercontent.com/u/135701940?v=4?s=100" width="100px;" alt="Kishan Parmar"/><br /><sub><b>Kishan Parmar</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=kishanprmr" title="Documentation">📖</a> <a href="#plugin-kishanprmr" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BBND"><img src="https://avatars.githubusercontent.com/u/42919338?v=4?s=100" width="100px;" alt="BBND"/><br /><sub><b>BBND</b></sub></a><br /><a href="#plugin-BBND" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haseebrehmanpc"><img src="https://avatars.githubusercontent.com/u/37938986?v=4?s=100" width="100px;" alt="Haseeb Rehman"/><br /><sub><b>Haseeb Rehman</b></sub></a><br /><a href="#plugin-haseebrehmanpc" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/ritagorokhod/"><img src="https://avatars.githubusercontent.com/u/60586879?v=4?s=100" width="100px;" alt="Rita Gorokhod"/><br /><sub><b>Rita Gorokhod</b></sub></a><br /><a href="#plugin-rita-gorokhod" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/facferreira"><img src="https://avatars.githubusercontent.com/u/487349?v=4?s=100" width="100px;" alt="Fábio Ferreira"/><br /><sub><b>Fábio Ferreira</b></sub></a><br /><a href="#plugin-facferreira" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://buffetitservices.ch"><img src="https://avatars.githubusercontent.com/u/73933252?v=4?s=100" width="100px;" alt="Florin Buffet"/><br /><sub><b>Florin Buffet</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=FlorinBuffet" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Owlcept"><img src="https://avatars.githubusercontent.com/u/67299472?v=4?s=100" width="100px;" alt="Drew Lewis"/><br /><sub><b>Drew Lewis</b></sub></a><br /><a href="#plugin-Owlcept" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://bendersej.com"><img src="https://avatars.githubusercontent.com/u/10613140?v=4?s=100" width="100px;" alt="Benjamin André-Micolon"/><br /><sub><b>Benjamin André-Micolon</b></sub></a><br /><a href="#plugin-bendersej" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DGurskij"><img src="https://avatars.githubusercontent.com/u/26856659?v=4?s=100" width="100px;" alt="Denis Gurskij"/><br /><sub><b>Denis Gurskij</b></sub></a><br /><a href="#plugin-DGurskij" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://neferlopez.com"><img src="https://avatars.githubusercontent.com/u/11466949?v=4?s=100" width="100px;" alt="Nefer Lopez"/><br /><sub><b>Nefer Lopez</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=thatguynef" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fardeenpanjwani-codeglo"><img src="https://avatars.githubusercontent.com/u/141914308?v=4?s=100" width="100px;" alt="fardeenpanjwani-codeglo"/><br /><sub><b>fardeenpanjwani-codeglo</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=fardeenpanjwani-codeglo" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/landonmoir"><img src="https://avatars.githubusercontent.com/u/29764668?v=4?s=100" width="100px;" alt="Landon Moir"/><br /><sub><b>Landon Moir</b></sub></a><br /><a href="#plugin-landonmoir" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://lightspeed-it.nl/"><img src="https://avatars.githubusercontent.com/u/22002313?v=4?s=100" width="100px;" alt="Diego Nijboer"/><br /><sub><b>Diego Nijboer</b></sub></a><br /><a href="#plugin-lldiegon" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://ductan.me/"><img src="https://avatars.githubusercontent.com/u/24206229?v=4?s=100" width="100px;" alt="Tân Một Nắng"/><br /><sub><b>Tân Một Nắng</b></sub></a><br /><a href="#plugin-tanoggy" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://geteduca.com"><img src="https://avatars.githubusercontent.com/u/838788?v=4?s=100" width="100px;" alt="Gavin Foley"/><br /><sub><b>Gavin Foley</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=GFoley83" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://dtrautwein.eu"><img src="https://avatars.githubusercontent.com/u/11836793?v=4?s=100" width="100px;" alt="Dennis Trautwein"/><br /><sub><b>Dennis Trautwein</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Adennis-tra" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/inspiredclick"><img src="https://avatars.githubusercontent.com/u/1548613?v=4?s=100" width="100px;" alt="Andrew Rosenblatt"/><br /><sub><b>Andrew Rosenblatt</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Ainspiredclick" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/w95"><img src="https://avatars.githubusercontent.com/u/6433752?v=4?s=100" width="100px;" alt="rika"/><br /><sub><b>rika</b></sub></a><br /><a href="#plugin-w95" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cyrilselasi"><img src="https://avatars.githubusercontent.com/u/7190330?v=4?s=100" width="100px;" alt="Cyril Selasi"/><br /><sub><b>Cyril Selasi</b></sub></a><br /><a href="#plugin-cyrilselasi" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://nijfranck.github.io"><img src="https://avatars.githubusercontent.com/u/9940307?v=4?s=100" width="100px;" alt="Franck Nijimbere"/><br /><sub><b>Franck Nijimbere</b></sub></a><br /><a href="#plugin-nijfranck" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alerdenisov"><img src="https://avatars.githubusercontent.com/u/3899837?v=4?s=100" width="100px;" alt="Aleksandr Denisov"/><br /><sub><b>Aleksandr Denisov</b></sub></a><br /><a href="#plugin-alerdenisov" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rbnswartz"><img src="https://avatars.githubusercontent.com/u/724704?v=4?s=100" width="100px;" alt="Reuben Swartz"/><br /><sub><b>Reuben Swartz</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=rbnswartz" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://lupianezjose.com"><img src="https://avatars.githubusercontent.com/u/4380557?v=4?s=100" width="100px;" alt="joselupianez"/><br /><sub><b>joselupianez</b></sub></a><br /><a href="#plugin-joselupianez" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.zidoary.com"><img src="https://avatars.githubusercontent.com/u/24081860?v=4?s=100" width="100px;" alt="Awais Manzoor"/><br /><sub><b>Awais Manzoor</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Aawais000" title="Bug reports">🐛</a> <a href="https://github.com/activepieces/activepieces/commits?author=awais000" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andchir"><img src="https://avatars.githubusercontent.com/u/6392311?v=4?s=100" width="100px;" alt="Andrei"/><br /><sub><b>Andrei</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Aandchir" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/derbbre"><img src="https://avatars.githubusercontent.com/u/281843?v=4?s=100" width="100px;" alt="derbbre"/><br /><sub><b>derbbre</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=derbbre" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/maor-rozenfeld"><img src="https://avatars.githubusercontent.com/u/49363375?v=4?s=100" width="100px;" alt="Maor Rozenfeld"/><br /><sub><b>Maor Rozenfeld</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=maor-rozenfeld" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miqh"><img src="https://avatars.githubusercontent.com/u/43751307?v=4?s=100" width="100px;" alt="Michael Huynh"/><br /><sub><b>Michael Huynh</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=miqh" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fdundjer"><img src="https://avatars.githubusercontent.com/u/17405319?v=4?s=100" width="100px;" alt="Filip Dunđer"/><br /><sub><b>Filip Dunđer</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=fdundjer" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://donthorp.net"><img src="https://avatars.githubusercontent.com/u/8629?v=4?s=100" width="100px;" alt="Don Thorp"/><br /><sub><b>Don Thorp</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=donthorp" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://joeworkman.net"><img src="https://avatars.githubusercontent.com/u/225628?v=4?s=100" width="100px;" alt="Joe Workman"/><br /><sub><b>Joe Workman</b></sub></a><br /><a href="#plugin-joeworkman" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Autumnlight02"><img src="https://avatars.githubusercontent.com/u/68244453?v=4?s=100" width="100px;" alt="Aykut Akgün"/><br /><sub><b>Aykut Akgün</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=Autumnlight02" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yann120"><img src="https://avatars.githubusercontent.com/u/10012140?v=4?s=100" width="100px;" alt="Yann Petitjean"/><br /><sub><b>Yann Petitjean</b></sub></a><br /><a href="#plugin-yann120" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/activepieces/activepieces/issues?q=author%3Ayann120" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pfernandez98"><img src="https://avatars.githubusercontent.com/u/54374282?v=4?s=100" width="100px;" alt="pfernandez98"/><br /><sub><b>pfernandez98</b></sub></a><br /><a href="#plugin-pfernandez98" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://denieler.com"><img src="https://avatars.githubusercontent.com/u/2836281?v=4?s=100" width="100px;" alt="Daniel O."/><br /><sub><b>Daniel O.</b></sub></a><br /><a href="#plugin-denieler" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://myh.tw"><img src="https://avatars.githubusercontent.com/u/12458706?v=4?s=100" width="100px;" alt="Meng-Yuan Huang"/><br /><sub><b>Meng-Yuan Huang</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=MrMYHuang" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bigfluffycookie"><img src="https://avatars.githubusercontent.com/u/54935347?v=4?s=100" width="100px;" alt="Leyla"/><br /><sub><b>Leyla</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Abigfluffycookie" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://i-nithin.netlify.app/"><img src="https://avatars.githubusercontent.com/u/97078688?v=4?s=100" width="100px;" alt="i-nithin"/><br /><sub><b>i-nithin</b></sub></a><br /><a href="#plugin-i-nithin" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://lawrenceli.me"><img src="https://avatars.githubusercontent.com/u/24540598?v=4?s=100" width="100px;" alt="la3rence"/><br /><sub><b>la3rence</b></sub></a><br /><a href="#plugin-la3rence" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://dennisrongo.com"><img src="https://avatars.githubusercontent.com/u/51771021?v=4?s=100" width="100px;" alt="Dennis Rongo"/><br /><sub><b>Dennis Rongo</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Adennisrongo" title="Bug reports">🐛</a> <a href="#plugin-dennisrongo" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kartikmehta8"><img src="https://avatars.githubusercontent.com/u/77505989?v=4?s=100" width="100px;" alt="Kartik Mehta"/><br /><sub><b>Kartik Mehta</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=kartikmehta8" title="Documentation">📖</a> <a href="https://github.com/activepieces/activepieces/commits?author=kartikmehta8" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://zakher.me"><img src="https://avatars.githubusercontent.com/u/46135573?v=4?s=100" width="100px;" alt="Zakher Masri"/><br /><sub><b>Zakher Masri</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=zaaakher" title="Documentation">📖</a> <a href="https://github.com/activepieces/activepieces/commits?author=zaaakher" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AbdullahBitar"><img src="https://avatars.githubusercontent.com/u/122645579?v=4?s=100" width="100px;" alt="AbdullahBitar"/><br /><sub><b>AbdullahBitar</b></sub></a><br /><a href="#plugin-AbdullahBitar" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mariomeyer"><img src="https://avatars.githubusercontent.com/u/867650?v=4?s=100" width="100px;" alt="Mario Meyer"/><br /><sub><b>Mario Meyer</b></sub></a><br /><a href="#plugin-mariomeyer" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/karimkhaleel"><img src="https://avatars.githubusercontent.com/u/94621779?v=4?s=100" width="100px;" alt="Karim Khaleel"/><br /><sub><b>Karim Khaleel</b></sub></a><br /><a href="#plugin-karimkhaleel" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CPonchet"><img src="https://avatars.githubusercontent.com/u/40756925?v=4?s=100" width="100px;" alt="CPonchet"/><br /><sub><b>CPonchet</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3ACPonchet" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AdamSelene"><img src="https://avatars.githubusercontent.com/u/79495?v=4?s=100" width="100px;" alt="Olivier Sambourg"/><br /><sub><b>Olivier Sambourg</b></sub></a><br /><a href="#plugin-AdamSelene" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Verlich"><img src="https://avatars.githubusercontent.com/u/30838131?v=4?s=100" width="100px;" alt="Ahmad(Ed)"/><br /><sub><b>Ahmad(Ed)</b></sub></a><br /><a href="#plugin-Verlich" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leenmashni"><img src="https://avatars.githubusercontent.com/u/102361544?v=4?s=100" width="100px;" alt="leenmashni"/><br /><sub><b>leenmashni</b></sub></a><br /><a href="#plugin-leenmashni" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AliasKingsWorth"><img src="https://avatars.githubusercontent.com/u/47811610?v=4?s=100" width="100px;" alt="M Abdul Rauf"/><br /><sub><b>M Abdul Rauf</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=AliasKingsWorth" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vbarrier"><img src="https://avatars.githubusercontent.com/u/446808?v=4?s=100" width="100px;" alt="Vincent Barrier"/><br /><sub><b>Vincent Barrier</b></sub></a><br /><a href="#plugin-vbarrier" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://johnmark.dev"><img src="https://avatars.githubusercontent.com/u/65794951?v=4?s=100" width="100px;" alt="John"/><br /><sub><b>John</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=jmgb27" title="Code">💻</a> <a href="#plugin-jmgb27" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://joost.blog/"><img src="https://avatars.githubusercontent.com/u/487629?v=4?s=100" width="100px;" alt="Joost de Valk"/><br /><sub><b>Joost de Valk</b></sub></a><br /><a href="#plugin-jdevalk" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/nyamkamunhjin/"><img src="https://avatars.githubusercontent.com/u/44439626?v=4?s=100" width="100px;" alt="MJ"/><br /><sub><b>MJ</b></sub></a><br /><a href="#plugin-nyamkamunhjin" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shravankshenoy"><img src="https://avatars.githubusercontent.com/u/29670290?v=4?s=100" width="100px;" alt="ShravanShenoy"/><br /><sub><b>ShravanShenoy</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=shravankshenoy" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://jonkristian.no"><img src="https://avatars.githubusercontent.com/u/13219?v=4?s=100" width="100px;" alt="Jon Kristian"/><br /><sub><b>Jon Kristian</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=jonkristian" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cr0fters"><img src="https://avatars.githubusercontent.com/u/1754858?v=4?s=100" width="100px;" alt="cr0fters"/><br /><sub><b>cr0fters</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Acr0fters" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://bibek-timsina.com.np/"><img src="https://avatars.githubusercontent.com/u/29589003?v=4?s=100" width="100px;" alt="Bibek Timsina"/><br /><sub><b>Bibek Timsina</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Abimsina" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/szepeviktor/debian-server-tools/blob/master/CV.md"><img src="https://avatars.githubusercontent.com/u/952007?v=4?s=100" width="100px;" alt="Viktor Szépe"/><br /><sub><b>Viktor Szépe</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=szepeviktor" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rendyt1"><img src="https://avatars.githubusercontent.com/u/38492810?v=4?s=100" width="100px;" alt="Rendy Tan"/><br /><sub><b>Rendy Tan</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=rendyt1" title="Documentation">📖</a> <a href="#plugin-rendyt1" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://islamaf.github.io"><img src="https://avatars.githubusercontent.com/u/44944648?v=4?s=100" width="100px;" alt="Islam Abdelfattah"/><br /><sub><b>Islam Abdelfattah</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Aislamaf" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/uniqueeest"><img src="https://avatars.githubusercontent.com/u/123538138?v=4?s=100" width="100px;" alt="Yoonjae Choi"/><br /><sub><b>Yoonjae Choi</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=uniqueeest" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://javix64.com"><img src="https://avatars.githubusercontent.com/u/58471170?v=4?s=100" width="100px;" alt="Javier HM"/><br /><sub><b>Javier HM</b></sub></a><br /><a href="#plugin-javix64" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://farag.tech"><img src="https://avatars.githubusercontent.com/u/50884619?v=4?s=100" width="100px;" alt="Mohamed Hassan"/><br /><sub><b>Mohamed Hassan</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3AMohamedHassan499" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.coasy.com/"><img src="https://avatars.githubusercontent.com/u/17610709?v=4?s=100" width="100px;" alt="Christian Schab"/><br /><sub><b>Christian Schab</b></sub></a><br /><a href="#plugin-christian-schab" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.gamespecifications.com/"><img src="https://avatars.githubusercontent.com/u/37847256?v=4?s=100" width="100px;" alt="Pratik Kinage"/><br /><sub><b>Pratik Kinage</b></sub></a><br /><a href="#plugin-thirstycode" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LevwTech"><img src="https://avatars.githubusercontent.com/u/69399787?v=4?s=100" width="100px;" alt="Abdelrahman Mostafa "/><br /><sub><b>Abdelrahman Mostafa </b></sub></a><br /><a href="#plugin-LevwTech" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HamzaZagha"><img src="https://avatars.githubusercontent.com/u/45468866?v=4?s=100" width="100px;" alt="Hamza Zagha"/><br /><sub><b>Hamza Zagha</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3AHamzaZagha" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://founderblocks.io/"><img src="https://avatars.githubusercontent.com/u/88160672?v=4?s=100" width="100px;" alt="Lasse Schuirmann"/><br /><sub><b>Lasse Schuirmann</b></sub></a><br /><a href="#plugin-founderblocks-sils" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://about.me/cyril_duchon_doris"><img src="https://avatars.githubusercontent.com/u/7388889?v=4?s=100" width="100px;" alt="Cyril Duchon-Doris"/><br /><sub><b>Cyril Duchon-Doris</b></sub></a><br /><a href="#plugin-Startouf" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Javiink"><img src="https://avatars.githubusercontent.com/u/43996484?v=4?s=100" width="100px;" alt="Javiink"/><br /><sub><b>Javiink</b></sub></a><br /><a href="#plugin-Javiink" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hharchani"><img src="https://avatars.githubusercontent.com/u/6430611?v=4?s=100" width="100px;" alt="Harshit Harchani"/><br /><sub><b>Harshit Harchani</b></sub></a><br /><a href="#plugin-hharchani" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrAkber"><img src="https://avatars.githubusercontent.com/u/170118042?v=4?s=100" width="100px;" alt="MrAkber"/><br /><sub><b>MrAkber</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=MrAkber" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marek-slavicek"><img src="https://avatars.githubusercontent.com/u/136325104?v=4?s=100" width="100px;" alt="marek-slavicek"/><br /><sub><b>marek-slavicek</b></sub></a><br /><a href="#plugin-marek-slavicek" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hugh-codes"><img src="https://avatars.githubusercontent.com/u/166336705?v=4?s=100" width="100px;" alt="hugh-codes"/><br /><sub><b>hugh-codes</b></sub></a><br /><a href="#plugin-hugh-codes" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alewis001"><img src="https://avatars.githubusercontent.com/u/3482446?v=4?s=100" width="100px;" alt="Alex Lewis"/><br /><sub><b>Alex Lewis</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Aalewis001" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://yual.in"><img src="https://avatars.githubusercontent.com/u/21105863?v=4?s=100" width="100px;" alt="Yuanlin Lin"/><br /><sub><b>Yuanlin Lin</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=yuaanlin" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://klo.dev"><img src="https://avatars.githubusercontent.com/u/96867907?v=4?s=100" width="100px;" alt="Ala Shiban"/><br /><sub><b>Ala Shiban</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=AlaShibanAtKlo" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://hamedsh.medium.com"><img src="https://avatars.githubusercontent.com/u/6043214?v=4?s=100" width="100px;" alt="hamsh"/><br /><sub><b>hamsh</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=hamedsh" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.anne-mariel.com/"><img src="https://avatars.githubusercontent.com/u/77142075?v=4?s=100" width="100px;" alt="Anne Mariel Catapang"/><br /><sub><b>Anne Mariel Catapang</b></sub></a><br /><a href="#plugin-AnneMariel95" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://hi.carlogino.com"><img src="https://avatars.githubusercontent.com/u/19299524?v=4?s=100" width="100px;" alt="Carlo Gino Catapang"/><br /><sub><b>Carlo Gino Catapang</b></sub></a><br /><a href="#plugin-codegino" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/drona2938"><img src="https://avatars.githubusercontent.com/u/34496554?v=4?s=100" width="100px;" alt="Aditya Rathore"/><br /><sub><b>Aditya Rathore</b></sub></a><br /><a href="#plugin-drona2938" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/coderbob2"><img src="https://avatars.githubusercontent.com/u/47177246?v=4?s=100" width="100px;" alt="coderbob2"/><br /><sub><b>coderbob2</b></sub></a><br /><a href="#plugin-coderbob2" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://raamyy.netlify.app"><img src="https://avatars.githubusercontent.com/u/29176293?v=4?s=100" width="100px;" alt="Ramy Gamal"/><br /><sub><b>Ramy Gamal</b></sub></a><br /><a href="#plugin-Raamyy" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://alexandrudanpop.dev/"><img src="https://avatars.githubusercontent.com/u/15979292?v=4?s=100" width="100px;" alt="Alexandru-Dan Pop"/><br /><sub><b>Alexandru-Dan Pop</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=alexandrudanpop" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Trayshmhirk"><img src="https://avatars.githubusercontent.com/u/112286458?v=4?s=100" width="100px;" alt="Frank Micheal "/><br /><sub><b>Frank Micheal </b></sub></a><br /><a href="#plugin-Trayshmhirk" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/emmanuel-ferdman"><img src="https://avatars.githubusercontent.com/u/35470921?v=4?s=100" width="100px;" alt="Emmanuel Ferdman"/><br /><sub><b>Emmanuel Ferdman</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=emmanuel-ferdman" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sany2407"><img src="https://avatars.githubusercontent.com/u/179091674?v=4?s=100" width="100px;" alt="Sany A"/><br /><sub><b>Sany A</b></sub></a><br /><a href="#plugin-sany2407" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://swimburger.net"><img src="https://avatars.githubusercontent.com/u/3382717?v=4?s=100" width="100px;" alt="Niels Swimberghe"/><br /><sub><b>Niels Swimberghe</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3ASwimburger" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lostinbug"><img src="https://avatars.githubusercontent.com/u/157452389?v=4?s=100" width="100px;" alt="lostinbug"/><br /><sub><b>lostinbug</b></sub></a><br /><a href="#plugin-lostinbug" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gushkool"><img src="https://avatars.githubusercontent.com/u/64713308?v=4?s=100" width="100px;" alt="gushkool"/><br /><sub><b>gushkool</b></sub></a><br /><a href="#plugin-gushkool" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.linkedin.com/in/omarsayed"><img src="https://avatars.githubusercontent.com/u/3813045?v=4?s=100" width="100px;" alt="Omar Sayed"/><br /><sub><b>Omar Sayed</b></sub></a><br /><a href="#plugin-OmarSayed" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rSnapkoOpenOps"><img src="https://avatars.githubusercontent.com/u/179845343?v=4?s=100" width="100px;" alt="rSnapkoOpenOps"/><br /><sub><b>rSnapkoOpenOps</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3ArSnapkoOpenOps" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ahronshor"><img src="https://avatars.githubusercontent.com/u/25138831?v=4?s=100" width="100px;" alt="ahronshor"/><br /><sub><b>ahronshor</b></sub></a><br /><a href="#plugin-ahronshor" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cezudas"><img src="https://avatars.githubusercontent.com/u/3786138?v=4?s=100" width="100px;" alt="Cezar"/><br /><sub><b>Cezar</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Acezudas" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geekyme-fsmk"><img src="https://avatars.githubusercontent.com/u/100678833?v=4?s=100" width="100px;" alt="Shawn Lim"/><br /><sub><b>Shawn Lim</b></sub></a><br /><a href="#plugin-geekyme-fsmk" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://shawn.storyline.io/"><img src="https://avatars.githubusercontent.com/u/977460?v=4?s=100" width="100px;" alt="Shawn Lim"/><br /><sub><b>Shawn Lim</b></sub></a><br /><a href="#plugin-geekyme" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pavloDeshko"><img src="https://avatars.githubusercontent.com/u/27104046?v=4?s=100" width="100px;" alt="pavloDeshko"/><br /><sub><b>pavloDeshko</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3ApavloDeshko" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liuhuapiaoyuan"><img src="https://avatars.githubusercontent.com/u/8020726?v=4?s=100" width="100px;" alt="abc"/><br /><sub><b>abc</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=liuhuapiaoyuan" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/manojkum-d"><img src="https://avatars.githubusercontent.com/u/141437046?v=4?s=100" width="100px;" alt="manoj kumar d"/><br /><sub><b>manoj kumar d</b></sub></a><br /><a href="#plugin-manojkum-d" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/felifluid"><img src="https://avatars.githubusercontent.com/u/59516203?v=4?s=100" width="100px;" alt="Feli"/><br /><sub><b>Feli</b></sub></a><br /><a href="#plugin-felifluid" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mordonez"><img src="https://avatars.githubusercontent.com/u/293837?v=4?s=100" width="100px;" alt="Miguel"/><br /><sub><b>Miguel</b></sub></a><br /><a href="#plugin-mordonez" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dev-instasent"><img src="https://avatars.githubusercontent.com/u/116744368?v=4?s=100" width="100px;" alt="Instasent DEV"/><br /><sub><b>Instasent DEV</b></sub></a><br /><a href="#plugin-dev-instasent" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/matthieu-lombard"><img src="https://avatars.githubusercontent.com/u/33624489?v=4?s=100" width="100px;" alt="Matthieu Lombard"/><br /><sub><b>Matthieu Lombard</b></sub></a><br /><a href="#plugin-matthieu-lombard" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/beyondlevi"><img src="https://avatars.githubusercontent.com/u/57486338?v=4?s=100" width="100px;" alt="beyondlevi"/><br /><sub><b>beyondlevi</b></sub></a><br /><a href="#plugin-beyondlevi" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://rafal.fyi"><img src="https://avatars.githubusercontent.com/u/10667346?v=4?s=100" width="100px;" alt="Rafal Zawadzki"/><br /><sub><b>Rafal Zawadzki</b></sub></a><br /><a href="#plugin-rafalzawadzki" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.pdfmonkey.io/"><img src="https://avatars.githubusercontent.com/u/119303?v=4?s=100" width="100px;" alt="Simon Courtois"/><br /><sub><b>Simon Courtois</b></sub></a><br /><a href="#plugin-simonc" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alegria-solutions"><img src="https://avatars.githubusercontent.com/u/124846022?v=4?s=100" width="100px;" alt="alegria-solutions"/><br /><sub><b>alegria-solutions</b></sub></a><br /><a href="#plugin-alegria-solutions" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/D-Rowe-FS"><img src="https://avatars.githubusercontent.com/u/142934784?v=4?s=100" width="100px;" alt="D-Rowe-FS"/><br /><sub><b>D-Rowe-FS</b></sub></a><br /><a href="#plugin-D-Rowe-FS" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChineseHamberger"><img src="https://avatars.githubusercontent.com/u/101547635?v=4?s=100" width="100px;" alt="张晟杰"/><br /><sub><b>张晟杰</b></sub></a><br /><a href="#plugin-ChineseHamberger" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://codesign.rf.gd"><img src="https://avatars.githubusercontent.com/u/72438085?v=4?s=100" width="100px;" alt="Ashot"/><br /><sub><b>Ashot</b></sub></a><br /><a href="#plugin-AshotZaqoyan" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/amrabuaza"><img src="https://avatars.githubusercontent.com/u/30035105?v=4?s=100" width="100px;" alt="Amr Abu Aza"/><br /><sub><b>Amr Abu Aza</b></sub></a><br /><a href="#plugin-amrabuaza" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://johng.io"><img src="https://avatars.githubusercontent.com/u/9030780?v=4?s=100" width="100px;" alt="John Goodliff"/><br /><sub><b>John Goodliff</b></sub></a><br /><a href="#plugin-jerboa88" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DiwashDev"><img src="https://avatars.githubusercontent.com/u/182864159?v=4?s=100" width="100px;" alt="Diwash Dev"/><br /><sub><b>Diwash Dev</b></sub></a><br /><a href="#plugin-DiwashDev" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.seven.io"><img src="https://avatars.githubusercontent.com/u/12965261?v=4?s=100" width="100px;" alt="André"/><br /><sub><b>André</b></sub></a><br /><a href="#plugin-matthiez" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loudotdigital"><img src="https://avatars.githubusercontent.com/u/7611772?v=4?s=100" width="100px;" alt="Lou | Digital Marketing"/><br /><sub><b>Lou | Digital Marketing</b></sub></a><br /><a href="#plugin-loudotdigital" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/maarteNNNN"><img src="https://avatars.githubusercontent.com/u/14275291?v=4?s=100" width="100px;" alt="Maarten Coppens"/><br /><sub><b>Maarten Coppens</b></sub></a><br /><a href="#plugin-maarteNNNN" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mahmuthamet"><img src="https://avatars.githubusercontent.com/u/90776946?v=4?s=100" width="100px;" alt="Mahmoud Hamed"/><br /><sub><b>Mahmoud Hamed</b></sub></a><br /><a href="#plugin-mahmuthamet" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://dammaretz.com"><img src="https://avatars.githubusercontent.com/u/14098167?v=4?s=100" width="100px;" alt="Theo Dammaretz"/><br /><sub><b>Theo Dammaretz</b></sub></a><br /><a href="#plugin-Blightwidow" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/s31w4n"><img src="https://avatars.githubusercontent.com/u/63353528?v=4?s=100" width="100px;" alt="s31w4n"/><br /><sub><b>s31w4n</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=s31w4n" title="Documentation">📖</a> <a href="https://github.com/activepieces/activepieces/commits?author=s31w4n" title="Code">💻</a> <a href="#plugin-s31w4n" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://kallabot.com"><img src="https://avatars.githubusercontent.com/u/94991678?v=4?s=100" width="100px;" alt="Abdul Rahman"/><br /><sub><b>Abdul Rahman</b></sub></a><br /><a href="#plugin-abdulrahmanmajid" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/coat"><img src="https://avatars.githubusercontent.com/u/1661?v=4?s=100" width="100px;" alt="Kent Smith"/><br /><sub><b>Kent Smith</b></sub></a><br /><a href="#plugin-coat" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ArvindEnvoy"><img src="https://avatars.githubusercontent.com/u/25014185?v=4?s=100" width="100px;" alt="Arvind Ramesh"/><br /><sub><b>Arvind Ramesh</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=ArvindEnvoy" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/valentin-mourtialon"><img src="https://avatars.githubusercontent.com/u/88686764?v=4?s=100" width="100px;" alt="valentin-mourtialon"/><br /><sub><b>valentin-mourtialon</b></sub></a><br /><a href="#plugin-valentin-mourtialon" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/psgpsg16"><img src="https://avatars.githubusercontent.com/u/188385621?v=4?s=100" width="100px;" alt="psgpsg16"/><br /><sub><b>psgpsg16</b></sub></a><br /><a href="#plugin-psgpsg16" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mariiawidrpay"><img src="https://avatars.githubusercontent.com/u/110456120?v=4?s=100" width="100px;" alt="Mariia Shyn"/><br /><sub><b>Mariia Shyn</b></sub></a><br /><a href="#plugin-mariiawidrpay" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/joshuaheslin"><img src="https://avatars.githubusercontent.com/u/48037470?v=4?s=100" width="100px;" alt="Joshua Heslin"/><br /><sub><b>Joshua Heslin</b></sub></a><br /><a href="#plugin-joshuaheslin" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ahmad-swanblocks"><img src="https://avatars.githubusercontent.com/u/165162455?v=4?s=100" width="100px;" alt="Ahmad"/><br /><sub><b>Ahmad</b></sub></a><br /><a href="#plugin-ahmad-swanblocks" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/danielpoonwj"><img src="https://avatars.githubusercontent.com/u/17039704?v=4?s=100" width="100px;" alt="Daniel Poon"/><br /><sub><b>Daniel Poon</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=danielpoonwj" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kevinyu-alan"><img src="https://avatars.githubusercontent.com/u/198612963?v=4?s=100" width="100px;" alt="Kévin Yu"/><br /><sub><b>Kévin Yu</b></sub></a><br /><a href="#plugin-Kevinyu-alan" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/flex-yeongeun"><img src="https://avatars.githubusercontent.com/u/186537288?v=4?s=100" width="100px;" alt="노영은"/><br /><sub><b>노영은</b></sub></a><br /><a href="#plugin-flex-yeongeun" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reemayoush"><img src="https://avatars.githubusercontent.com/u/168414383?v=4?s=100" width="100px;" alt="reemayoush"/><br /><sub><b>reemayoush</b></sub></a><br /><a href="#plugin-reemayoush" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/briceflaceliere"><img src="https://avatars.githubusercontent.com/u/5811531?v=4?s=100" width="100px;" alt="Brice"/><br /><sub><b>Brice</b></sub></a><br /><a href="#security-briceflaceliere" title="Security">🛡️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://mg-wunna.github.io/mg-wunna/"><img src="https://avatars.githubusercontent.com/u/63114419?v=4?s=100" width="100px;" alt="Mg Wunna"/><br /><sub><b>Mg Wunna</b></sub></a><br /><a href="#plugin-mg-wunna" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/harikrishnanum/"><img src="https://avatars.githubusercontent.com/u/61736905?v=4?s=100" width="100px;" alt="Harikrishnan U M"/><br /><sub><b>Harikrishnan U M</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/issues?q=author%3Ahakrsh" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/perrine-pullicino-alan"><img src="https://avatars.githubusercontent.com/u/143406842?v=4?s=100" width="100px;" alt="perrine-pullicino-alan"/><br /><sub><b>perrine-pullicino-alan</b></sub></a><br /><a href="#plugin-perrine-pullicino-alan" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://kaovilai.pw"><img src="https://avatars.githubusercontent.com/u/11228024?v=4?s=100" width="100px;" alt="Tiger Kaovilai"/><br /><sub><b>Tiger Kaovilai</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=kaovilai" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CarefulGuru"><img src="https://avatars.githubusercontent.com/u/141072854?v=4?s=100" width="100px;" alt="CarefulGuru"/><br /><sub><b>CarefulGuru</b></sub></a><br /><a href="#plugin-CarefulGuru" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AnkitSharmaOnGithub"><img src="https://avatars.githubusercontent.com/u/53289186?v=4?s=100" width="100px;" alt="Ankit Kumar Sharma"/><br /><sub><b>Ankit Kumar Sharma</b></sub></a><br /><a href="#plugin-AnkitSharmaOnGithub" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nhnansari"><img src="https://avatars.githubusercontent.com/u/116841234?v=4?s=100" width="100px;" alt="Naeem Hassan"/><br /><sub><b>Naeem Hassan</b></sub></a><br /><a href="#plugin-nhnansari" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://timpetricola.com"><img src="https://avatars.githubusercontent.com/u/674084?v=4?s=100" width="100px;" alt="Tim Petricola"/><br /><sub><b>Tim Petricola</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=TimPetricola" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/amaan-ai20"><img src="https://avatars.githubusercontent.com/u/188329978?v=4?s=100" width="100px;" alt="Amaan"/><br /><sub><b>Amaan</b></sub></a><br /><a href="#plugin-amaan-ai20" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.marcllopartriera.com"><img src="https://avatars.githubusercontent.com/u/1257083?v=4?s=100" width="100px;" alt="Marc Llopart"/><br /><sub><b>Marc Llopart</b></sub></a><br /><a href="#plugin-mllopart" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/onyedikachi-david"><img src="https://avatars.githubusercontent.com/u/51977119?v=4?s=100" width="100px;" alt="David Anyatonwu"/><br /><sub><b>David Anyatonwu</b></sub></a><br /><a href="#plugin-onyedikachi-david" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://huzef.com"><img src="https://avatars.githubusercontent.com/u/62795688?v=4?s=100" width="100px;" alt="neo773"/><br /><sub><b>neo773</b></sub></a><br /><a href="#plugin-neo773" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Daniel-Klippa"><img src="https://avatars.githubusercontent.com/u/207180643?v=4?s=100" width="100px;" alt="Daniel-Klippa"/><br /><sub><b>Daniel-Klippa</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=Daniel-Klippa" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/krushnarout"><img src="https://avatars.githubusercontent.com/u/129386740?v=4?s=100" width="100px;" alt="Krushna Kanta Rout"/><br /><sub><b>Krushna Kanta Rout</b></sub></a><br /><a href="#plugin-krushnarout" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.snimesh.com"><img src="https://avatars.githubusercontent.com/u/12984120?v=4?s=100" width="100px;" alt="Nimesh Solanki"/><br /><sub><b>Nimesh Solanki</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=nish17" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rimjhimyadav"><img src="https://avatars.githubusercontent.com/u/187646079?v=4?s=100" width="100px;" alt="Rimjhim Yadav"/><br /><sub><b>Rimjhim Yadav</b></sub></a><br /><a href="#plugin-rimjhimyadav" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gs03-dev"><img src="https://avatars.githubusercontent.com/u/70076620?v=4?s=100" width="100px;" alt="gs03"/><br /><sub><b>gs03</b></sub></a><br /><a href="#plugin-gs03-dev" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kunal-Darekar"><img src="https://avatars.githubusercontent.com/u/150500530?v=4?s=100" width="100px;" alt="Kunal Darekar"/><br /><sub><b>Kunal Darekar</b></sub></a><br /><a href="#plugin-Kunal-Darekar" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Sanket6652"><img src="https://avatars.githubusercontent.com/u/119039046?v=4?s=100" width="100px;" alt="Sanket6652"/><br /><sub><b>Sanket6652</b></sub></a><br /><a href="#plugin-Sanket6652" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ani-4x"><img src="https://avatars.githubusercontent.com/u/174266491?v=4?s=100" width="100px;" alt="Animesh"/><br /><sub><b>Animesh</b></sub></a><br /><a href="#plugin-Ani-4x" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://tarvent.com"><img src="https://avatars.githubusercontent.com/u/13419128?v=4?s=100" width="100px;" alt="Derek Johnson"/><br /><sub><b>Derek Johnson</b></sub></a><br /><a href="#plugin-DerekJDev" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mamiefurax"><img src="https://avatars.githubusercontent.com/u/3955802?v=4?s=100" width="100px;" alt="MamieFurax"/><br /><sub><b>MamieFurax</b></sub></a><br /><a href="#plugin-mamiefurax" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jadhavharshh"><img src="https://avatars.githubusercontent.com/u/182950168?v=4?s=100" width="100px;" alt="Harsh Jadhav"/><br /><sub><b>Harsh Jadhav</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=jadhavharshh" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/lucaslima_souza"><img src="https://avatars.githubusercontent.com/u/1576846?v=4?s=100" width="100px;" alt="Lucas Lima de Souza"/><br /><sub><b>Lucas Lima de Souza</b></sub></a><br /><a href="#plugin-lucaslimasouza" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BenjaminAlan"><img src="https://avatars.githubusercontent.com/u/42831606?v=4?s=100" width="100px;" alt="Benjamin Benouarka"/><br /><sub><b>Benjamin Benouarka</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=BenjaminAlan" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/chris-wu/"><img src="https://avatars.githubusercontent.com/u/4491213?v=4?s=100" width="100px;" alt="Chris Wu"/><br /><sub><b>Chris Wu</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=cjwooo" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/prasanna2000-max"><img src="https://avatars.githubusercontent.com/u/61037314?v=4?s=100" width="100px;" alt="Prasanna R"/><br /><sub><b>Prasanna R</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=prasanna2000-max" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AdminCraftHD"><img src="https://avatars.githubusercontent.com/u/33310274?v=4?s=100" width="100px;" alt="AdminCraftHD"/><br /><sub><b>AdminCraftHD</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=AdminCraftHD" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://tumrabertworld.vercel.app/resume"><img src="https://avatars.githubusercontent.com/u/38305310?v=4?s=100" width="100px;" alt="Tanakit Phentun"/><br /><sub><b>Tanakit Phentun</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=tumrabert" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marapper"><img src="https://avatars.githubusercontent.com/u/1397054?v=4?s=100" width="100px;" alt="Sergey Bondar"/><br /><sub><b>Sergey Bondar</b></sub></a><br /><a href="#plugin-marapper" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://yusufcirak.net"><img src="https://avatars.githubusercontent.com/u/81169996?v=4?s=100" width="100px;" alt="Yusuf Çırak"/><br /><sub><b>Yusuf Çırak</b></sub></a><br /><a href="#plugin-yusuf-cirak" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://ezhil.dev"><img src="https://avatars.githubusercontent.com/u/103899034?v=4?s=100" width="100px;" alt="Ezhil Shanmugham"/><br /><sub><b>Ezhil Shanmugham</b></sub></a><br /><a href="#plugin-ezhil56x" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sunorains"><img src="https://avatars.githubusercontent.com/u/211304820?v=4?s=100" width="100px;" alt="Anderson"/><br /><sub><b>Anderson</b></sub></a><br /><a href="#plugin-sunorains" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/calladodan"><img src="https://avatars.githubusercontent.com/u/7246416?v=4?s=100" width="100px;" alt="Daniel Callado"/><br /><sub><b>Daniel Callado</b></sub></a><br /><a href="#plugin-calladodan" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sparkybug"><img src="https://avatars.githubusercontent.com/u/52334088?v=4?s=100" width="100px;" alt="Ukaegbu Osinachi"/><br /><sub><b>Ukaegbu Osinachi</b></sub></a><br /><a href="#plugin-sparkybug" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://mavrick-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/146999057?v=4?s=100" width="100px;" alt="Rishi Mondal"/><br /><sub><b>Rishi Mondal</b></sub></a><br /><a href="#plugin-MAVRICK-1" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cloudieunnie"><img src="https://avatars.githubusercontent.com/u/178718590?v=4?s=100" width="100px;" alt="Cloudieunnie"/><br /><sub><b>Cloudieunnie</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=Cloudieunnie" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/balwant1707"><img src="https://avatars.githubusercontent.com/u/17769387?v=4?s=100" width="100px;" alt="Balwant Singh"/><br /><sub><b>Balwant Singh</b></sub></a><br /><a href="#plugin-balwant1707" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ravikiranvm"><img src="https://avatars.githubusercontent.com/u/36404296?v=4?s=100" width="100px;" alt="Ravi Kiran Vallamkonda"/><br /><sub><b>Ravi Kiran Vallamkonda</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=ravikiranvm" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/owuzo"><img src="https://avatars.githubusercontent.com/u/173556464?v=4?s=100" width="100px;" alt="Owuzo Joy"/><br /><sub><b>Owuzo Joy</b></sub></a><br /><a href="#plugin-owuzo" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/helenedurand-smet"><img src="https://avatars.githubusercontent.com/u/206527847?v=4?s=100" width="100px;" alt="helenedurand-smet"/><br /><sub><b>helenedurand-smet</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=helenedurand-smet" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nuvex-dev"><img src="https://avatars.githubusercontent.com/u/181783827?v=4?s=100" width="100px;" alt="Nuvex"/><br /><sub><b>Nuvex</b></sub></a><br /><a href="#plugin-nuvex-dev" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zebi15"><img src="https://avatars.githubusercontent.com/u/80625145?v=4?s=100" width="100px;" alt="Apostol Eusebiu"/><br /><sub><b>Apostol Eusebiu</b></sub></a><br /><a href="#plugin-Zebi15" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fortunamide"><img src="https://avatars.githubusercontent.com/u/122938302?v=4?s=100" width="100px;" alt="Fortunate"/><br /><sub><b>Fortunate</b></sub></a><br /><a href="#plugin-fortunamide" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://jaachiwrites.com"><img src="https://avatars.githubusercontent.com/u/173014495?v=4?s=100" width="100px;" alt="Jaachịmmá Anyatọnwụ"/><br /><sub><b>Jaachịmmá Anyatọnwụ</b></sub></a><br /><a href="#plugin-thejaachi" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://luizmainart.com"><img src="https://avatars.githubusercontent.com/u/55499897?v=4?s=100" width="100px;" alt="Luiz D. M. Mainart"/><br /><sub><b>Luiz D. M. Mainart</b></sub></a><br /><a href="#plugin-LuizDMM" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/uvenkatateja"><img src="https://avatars.githubusercontent.com/u/118493739?v=4?s=100" width="100px;" alt="uvenkatateja"/><br /><sub><b>uvenkatateja</b></sub></a><br /><a href="#plugin-uvenkatateja" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://akramcodez.tech"><img src="https://avatars.githubusercontent.com/u/179671552?v=4?s=100" width="100px;" alt="SK Akram"/><br /><sub><b>SK Akram</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=akramcodez" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/im-soohyun"><img src="https://avatars.githubusercontent.com/u/127273427?v=4?s=100" width="100px;" alt="Kim SooHyun"/><br /><sub><b>Kim SooHyun</b></sub></a><br /><a href="#security-im-soohyun" title="Security">🛡️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/srimalleswari205"><img src="https://avatars.githubusercontent.com/u/235991945?v=4?s=100" width="100px;" alt="Sri malleswari"/><br /><sub><b>Sri malleswari</b></sub></a><br /><a href="#plugin-srimalleswari205" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SinghaAnirban005"><img src="https://avatars.githubusercontent.com/u/143536290?v=4?s=100" width="100px;" alt="Anirban Singha"/><br /><sub><b>Anirban Singha</b></sub></a><br /><a href="#plugin-SinghaAnirban005" title="Plugin/utility libraries">🔌</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://dania.es"><img src="https://avatars.githubusercontent.com/u/11240307?v=4?s=100" width="100px;" alt="Dania Es"/><br /><sub><b>Dania Es</b></sub></a><br /><a href="https://github.com/activepieces/activepieces/commits?author=Dania02525" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
|
This project follows the [all-contributors](https://allcontributors.org) specification.
|
||||||
|
Contributions of any kind are welcome!
|
||||||
39
activepieces-fork/SECURITY.md
Normal file
39
activepieces-fork/SECURITY.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Security
|
||||||
|
|
||||||
|
Contact: security@activepieces.com
|
||||||
|
|
||||||
|
Based on [https://supabase.com/.well-known/security.txt](https://supabase.com/.well-known/security.txt)
|
||||||
|
|
||||||
|
At Activepieces.com, we consider the security of our systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
|
||||||
|
|
||||||
|
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our clients and our systems.
|
||||||
|
|
||||||
|
## Out of scope vulnerabilities:
|
||||||
|
|
||||||
|
- Clickjacking on pages with no sensitive actions.
|
||||||
|
- Unauthenticated/logout/login CSRF.
|
||||||
|
- Attacks requiring MITM or physical access to a user's device.
|
||||||
|
- Any activity that could lead to the disruption of our service (DoS).
|
||||||
|
- Content spoofing and text injection issues without showing an attack vector/without being able to modify HTML/CSS.
|
||||||
|
- Email spoofing
|
||||||
|
- Missing DNSSEC, CAA, CSP headers
|
||||||
|
- Lack of Secure or HTTP only flag on non-sensitive cookies
|
||||||
|
- Deadlinks
|
||||||
|
|
||||||
|
## Please do the following:
|
||||||
|
|
||||||
|
- E-mail your findings to [security@activepieces.com](mailto:security@activepieces.com).
|
||||||
|
- Do not run automated scanners on our infrastructure or dashboard. If you wish to do this, contact us and we will set up a sandbox for you.
|
||||||
|
- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data,
|
||||||
|
- Do not reveal the problem to others until it has been resolved,
|
||||||
|
- Do not use attacks on physical security, social engineering, distributed denial of service, spam or applications of third parties,
|
||||||
|
- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible. Usually, the IP address or the URL of the affected system and a description of the vulnerability will be sufficient, but complex vulnerabilities may require further explanation.
|
||||||
|
|
||||||
|
## What we promise:
|
||||||
|
|
||||||
|
- We will respond to your report within 3 business days with our evaluation of the report and an expected resolution date,
|
||||||
|
- If you have followed the instructions above, we will not take any legal action against you in regard to the report,
|
||||||
|
- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission,
|
||||||
|
- We will keep you informed of the progress towards resolving the problem,
|
||||||
|
- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise), and
|
||||||
|
- We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved.
|
||||||
BIN
activepieces-fork/assets/ap-logo.png
Executable file
BIN
activepieces-fork/assets/ap-logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
11167
activepieces-fork/bun.lock
Normal file
11167
activepieces-fork/bun.lock
Normal file
File diff suppressed because it is too large
Load Diff
1
activepieces-fork/commitlint.config.js
Normal file
1
activepieces-fork/commitlint.config.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = { extends: ['@commitlint/config-conventional'] };
|
||||||
12
activepieces-fork/crowdin.yml
Normal file
12
activepieces-fork/crowdin.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
project_id_env: CROWDIN_PROJECT_ID
|
||||||
|
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||||
|
base_path: .
|
||||||
|
base_url: 'https://api.crowdin.com'
|
||||||
|
preserve_hierarchy: 1
|
||||||
|
files:
|
||||||
|
- type: i18next_json
|
||||||
|
source: packages/react-ui/public/locales/en/translation.json
|
||||||
|
translation: /packages/react-ui/public/locales/%two_letters_code%/translation.json
|
||||||
|
- type: json
|
||||||
|
source: packages/pieces/**/**/src/i18n/translation.json
|
||||||
|
translation: /packages/pieces/**/**/src/i18n/%two_letters_code%.json
|
||||||
5
activepieces-fork/deploy/activepieces-helm/.gitignore
vendored
Normal file
5
activepieces-fork/deploy/activepieces-helm/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Ignore downloaded Helm chart dependencies
|
||||||
|
charts/*.tgz
|
||||||
|
|
||||||
|
# Ignore Chart.lock file variations
|
||||||
|
Chart.lock
|
||||||
23
activepieces-fork/deploy/activepieces-helm/.helmignore
Normal file
23
activepieces-fork/deploy/activepieces-helm/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
39
activepieces-fork/deploy/activepieces-helm/Chart.yaml
Normal file
39
activepieces-fork/deploy/activepieces-helm/Chart.yaml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: activepieces
|
||||||
|
description: A Helm chart for Activepieces
|
||||||
|
icon: https://cdn.activepieces.com/brand/logo.svg
|
||||||
|
|
||||||
|
# A chart can be either an 'application' or a 'library' chart.
|
||||||
|
#
|
||||||
|
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||||
|
# to be deployed.
|
||||||
|
#
|
||||||
|
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||||
|
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||||
|
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||||
|
type: application
|
||||||
|
|
||||||
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
|
# to the chart and its templates, including the app version.
|
||||||
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
|
version: 0.3.0
|
||||||
|
|
||||||
|
# This is the version number of the application being deployed. This version number should be
|
||||||
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
|
# It is recommended to use it with quotes.
|
||||||
|
appVersion: "0.71.2"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- name: kubernetes-secret-generator
|
||||||
|
version: "3.4.1"
|
||||||
|
repository: "https://helm.mittwald.de"
|
||||||
|
condition: kubernetes-secret-generator.enabled
|
||||||
|
- name: postgresql
|
||||||
|
version: "11.7.2"
|
||||||
|
repository: "https://charts.bitnami.com/bitnami"
|
||||||
|
condition: postgresql.enabled
|
||||||
|
- name: redis
|
||||||
|
version: "17.4.2"
|
||||||
|
repository: "https://charts.bitnami.com/bitnami"
|
||||||
|
condition: redis.enabled
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Activepieces has been deployed successfully!
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "activepieces.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "activepieces.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "activepieces.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "activepieces.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "activepieces.chart" . }}
|
||||||
|
{{ include "activepieces.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "activepieces.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "activepieces.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "activepieces.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "activepieces.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "activepieces.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "activepieces.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
{{- if not .Values.autoscaling.enabled }}
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
{{- end }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "activepieces.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "activepieces.labels" . | nindent 8 }}
|
||||||
|
{{- with .Values.podLabels }}
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ include "activepieces.serviceAccountName" . }}
|
||||||
|
{{- with .Values.podSecurityContext }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
{{- with .Values.securityContext }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: {{ .Values.container.port }}
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
# Core Activepieces configuration
|
||||||
|
{{- if .Values.activepieces.frontendUrl }}
|
||||||
|
- name: AP_FRONTEND_URL
|
||||||
|
value: {{ .Values.activepieces.frontendUrl | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.apiKey }}
|
||||||
|
- name: AP_API_KEY
|
||||||
|
value: {{ .Values.activepieces.apiKey | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.clientRealIpHeader }}
|
||||||
|
- name: AP_CLIENT_REAL_IP_HEADER
|
||||||
|
value: {{ .Values.activepieces.clientRealIpHeader | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.configPath }}
|
||||||
|
- name: AP_CONFIG_PATH
|
||||||
|
value: {{ .Values.activepieces.configPath | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.devPieces }}
|
||||||
|
- name: AP_DEV_PIECES
|
||||||
|
value: {{ .Values.activepieces.devPieces | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.internalUrl }}
|
||||||
|
- name: AP_INTERNAL_URL
|
||||||
|
value: {{ .Values.activepieces.internalUrl | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.maxConcurrentJobsPerProject }}
|
||||||
|
- name: AP_MAX_CONCURRENT_JOBS_PER_PROJECT
|
||||||
|
value: {{ .Values.activepieces.maxConcurrentJobsPerProject | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.perplexityBaseUrl }}
|
||||||
|
- name: AP_PERPLEXITY_BASE_URL
|
||||||
|
value: {{ .Values.activepieces.perplexityBaseUrl | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.piecesSyncMode }}
|
||||||
|
- name: AP_PIECES_SYNC_MODE
|
||||||
|
value: {{ .Values.activepieces.piecesSyncMode | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.featurebaseApiKey }}
|
||||||
|
- name: AP_FEATUREBASE_API_KEY
|
||||||
|
value: {{ .Values.activepieces.featurebaseApiKey | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.enableFlowOnPublish }}
|
||||||
|
- name: AP_ENABLE_FLOW_ON_PUBLISH
|
||||||
|
value: {{ .Values.activepieces.enableFlowOnPublish | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.issueArchiveDays }}
|
||||||
|
- name: AP_ISSUE_ARCHIVE_DAYS
|
||||||
|
value: {{ .Values.activepieces.issueArchiveDays | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.pausedFlowTimeoutDays }}
|
||||||
|
- name: AP_PAUSED_FLOW_TIMEOUT_DAYS
|
||||||
|
value: {{ .Values.activepieces.pausedFlowTimeoutDays | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.logLevel }}
|
||||||
|
- name: AP_LOG_LEVEL
|
||||||
|
value: {{ .Values.activepieces.logLevel | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.logPretty }}
|
||||||
|
- name: AP_LOG_PRETTY
|
||||||
|
value: {{ .Values.activepieces.logPretty | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.appWebhookSecrets }}
|
||||||
|
- name: AP_APP_WEBHOOK_SECRETS
|
||||||
|
value: {{ .Values.activepieces.appWebhookSecrets | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.maxFileSizeMb }}
|
||||||
|
- name: AP_MAX_FILE_SIZE_MB
|
||||||
|
value: {{ .Values.activepieces.maxFileSizeMb | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.sandboxMemoryLimit }}
|
||||||
|
- name: AP_SANDBOX_MEMORY_LIMIT
|
||||||
|
value: {{ .Values.activepieces.sandboxMemoryLimit | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.sandboxPropagatedEnvVars }}
|
||||||
|
- name: AP_SANDBOX_PROPAGATED_ENV_VARS
|
||||||
|
value: {{ .Values.activepieces.sandboxPropagatedEnvVars | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.piecesSource }}
|
||||||
|
- name: AP_PIECES_SOURCE
|
||||||
|
value: {{ .Values.activepieces.piecesSource | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.apiRateLimiting.authn.enabled }}
|
||||||
|
- name: AP_API_RATE_LIMIT_AUTHN_ENABLED
|
||||||
|
value: {{ .Values.activepieces.apiRateLimiting.authn.enabled | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.apiRateLimiting.authn.max }}
|
||||||
|
- name: AP_API_RATE_LIMIT_AUTHN_MAX
|
||||||
|
value: {{ .Values.activepieces.apiRateLimiting.authn.max | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.apiRateLimiting.authn.window }}
|
||||||
|
- name: AP_API_RATE_LIMIT_AUTHN_WINDOW
|
||||||
|
value: {{ .Values.activepieces.apiRateLimiting.authn.window | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.projectRateLimiter.enabled }}
|
||||||
|
- name: AP_PROJECT_RATE_LIMITER_ENABLED
|
||||||
|
value: {{ .Values.activepieces.projectRateLimiter.enabled | quote }}
|
||||||
|
{{- end }}
|
||||||
|
- name: AP_ENCRYPTION_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ include "activepieces.fullname" . }}-secrets
|
||||||
|
key: encryption-key
|
||||||
|
- name: AP_JWT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ include "activepieces.fullname" . }}-jwt-secret
|
||||||
|
key: jwt-secret
|
||||||
|
{{- if .Values.activepieces.environment }}
|
||||||
|
- name: AP_ENVIRONMENT
|
||||||
|
value: {{ .Values.activepieces.environment | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.edition }}
|
||||||
|
- name: AP_EDITION
|
||||||
|
value: {{ .Values.activepieces.edition | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.executionMode }}
|
||||||
|
- name: AP_EXECUTION_MODE
|
||||||
|
value: {{ .Values.activepieces.executionMode | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.telemetryEnabled }}
|
||||||
|
- name: AP_TELEMETRY_ENABLED
|
||||||
|
value: {{ .Values.activepieces.telemetryEnabled | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.templatesSourceUrl }}
|
||||||
|
- name: AP_TEMPLATES_SOURCE_URL
|
||||||
|
value: {{ .Values.activepieces.templatesSourceUrl | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.flowWorkerConcurrency }}
|
||||||
|
- name: AP_FLOW_WORKER_CONCURRENCY
|
||||||
|
value: {{ .Values.activepieces.flowWorkerConcurrency | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.scheduledWorkerConcurrency }}
|
||||||
|
- name: AP_SCHEDULED_WORKER_CONCURRENCY
|
||||||
|
value: {{ .Values.activepieces.scheduledWorkerConcurrency | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.triggerDefaultPollInterval }}
|
||||||
|
- name: AP_TRIGGER_DEFAULT_POLL_INTERVAL
|
||||||
|
value: {{ .Values.activepieces.triggerDefaultPollInterval | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.triggerTimeoutSeconds }}
|
||||||
|
- name: AP_TRIGGER_TIMEOUT_SECONDS
|
||||||
|
value: {{ .Values.activepieces.triggerTimeoutSeconds | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.flowTimeoutSeconds }}
|
||||||
|
- name: AP_FLOW_TIMEOUT_SECONDS
|
||||||
|
value: {{ .Values.activepieces.flowTimeoutSeconds | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.webhookTimeoutSeconds }}
|
||||||
|
- name: AP_WEBHOOK_TIMEOUT_SECONDS
|
||||||
|
value: {{ .Values.activepieces.webhookTimeoutSeconds | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.executionDataRetentionDays }}
|
||||||
|
- name: AP_EXECUTION_DATA_RETENTION_DAYS
|
||||||
|
value: {{ .Values.activepieces.executionDataRetentionDays | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if or .Values.postgresql.enabled (or .Values.postgresql.host .Values.postgresql.url) }}
|
||||||
|
# PostgreSQL configuration (subchart or external)
|
||||||
|
- name: AP_DB_TYPE
|
||||||
|
value: "POSTGRES"
|
||||||
|
{{- if .Values.postgresql.auth.database }}
|
||||||
|
- name: AP_POSTGRES_DATABASE
|
||||||
|
value: {{ .Values.postgresql.auth.database | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.postgresql.host }}
|
||||||
|
- name: AP_POSTGRES_HOST
|
||||||
|
value: {{ .Values.postgresql.host | quote }}
|
||||||
|
{{- else if .Values.postgresql.enabled }}
|
||||||
|
- name: AP_POSTGRES_HOST
|
||||||
|
value: {{ printf "%s-postgresql" (include "activepieces.fullname" .) | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.postgresql.port }}
|
||||||
|
- name: AP_POSTGRES_PORT
|
||||||
|
value: {{ .Values.postgresql.port | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.postgresql.auth.username }}
|
||||||
|
- name: AP_POSTGRES_USERNAME
|
||||||
|
value: {{ .Values.postgresql.auth.username | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.postgresql.auth.externalSecret }}
|
||||||
|
- name: AP_POSTGRES_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ .Values.postgresql.auth.externalSecret.name | quote }}
|
||||||
|
key: {{ .Values.postgresql.auth.externalSecret.key | quote }}
|
||||||
|
{{- else if .Values.postgresql.auth.password }}
|
||||||
|
- name: AP_POSTGRES_PASSWORD
|
||||||
|
value: {{ .Values.postgresql.auth.password | quote }}
|
||||||
|
{{- else if .Values.postgresql.enabled }}
|
||||||
|
- name: AP_POSTGRES_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ include "activepieces.fullname" . }}-postgresql
|
||||||
|
key: postgres-password
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.postgresql.useSSL }}
|
||||||
|
- name: AP_POSTGRES_USE_SSL
|
||||||
|
value: {{ .Values.postgresql.useSSL | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.postgresql.sslCa }}
|
||||||
|
- name: AP_POSTGRES_SSL_CA
|
||||||
|
value: {{ .Values.postgresql.sslCa | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.postgresql.url }}
|
||||||
|
- name: AP_POSTGRES_URL
|
||||||
|
value: {{ .Values.postgresql.url | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
# SQLite configuration (fallback)
|
||||||
|
- name: AP_DB_TYPE
|
||||||
|
value: "SQLITE3"
|
||||||
|
{{- end }}
|
||||||
|
{{- if or .Values.redis.enabled (or .Values.redis.host .Values.redis.url) }}
|
||||||
|
# Redis configuration (subchart or external)
|
||||||
|
{{- if .Values.redis.host }}
|
||||||
|
- name: AP_REDIS_HOST
|
||||||
|
value: {{ .Values.redis.host | quote }}
|
||||||
|
{{- else if .Values.redis.enabled }}
|
||||||
|
- name: AP_REDIS_HOST
|
||||||
|
value: {{ printf "%s-redis-master" (include "activepieces.fullname" .) | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.port }}
|
||||||
|
- name: AP_REDIS_PORT
|
||||||
|
value: {{ .Values.redis.port | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.useSSL }}
|
||||||
|
- name: AP_REDIS_USE_SSL
|
||||||
|
value: {{ .Values.redis.useSSL | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.sslCaFile }}
|
||||||
|
- name: AP_REDIS_SSL_CA_FILE
|
||||||
|
value: {{ .Values.redis.sslCaFile | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.db }}
|
||||||
|
- name: AP_REDIS_DB
|
||||||
|
value: {{ .Values.redis.db | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.user }}
|
||||||
|
- name: AP_REDIS_USER
|
||||||
|
value: {{ .Values.redis.user | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.url }}
|
||||||
|
- name: AP_REDIS_URL
|
||||||
|
value: {{ .Values.redis.url | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if eq .Values.redis.type "sentinel" }}
|
||||||
|
- name: AP_REDIS_TYPE
|
||||||
|
value: "SENTINEL"
|
||||||
|
{{- else }}
|
||||||
|
# Default to DEFAULT for standalone Redis
|
||||||
|
- name: AP_REDIS_TYPE
|
||||||
|
value: "DEFAULT"
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.sentinel.role }}
|
||||||
|
- name: AP_REDIS_SENTINEL_ROLE
|
||||||
|
value: {{ .Values.redis.sentinel.role | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.sentinel.hosts }}
|
||||||
|
- name: AP_REDIS_SENTINEL_HOSTS
|
||||||
|
value: {{ .Values.redis.sentinel.hosts | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.sentinel.name }}
|
||||||
|
- name: AP_REDIS_SENTINEL_NAME
|
||||||
|
value: {{ .Values.redis.sentinel.name | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.failedJob.retentionDays }}
|
||||||
|
- name: AP_REDIS_FAILED_JOB_RETENTION_DAYS
|
||||||
|
value: {{ .Values.redis.failedJob.retentionDays | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.failedJob.retentionMaxCount }}
|
||||||
|
- name: AP_REDIS_FAILED_JOB_RETENTION_MAX_COUNT
|
||||||
|
value: {{ .Values.redis.failedJob.retentionMaxCount | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.redis.auth.externalSecret }}
|
||||||
|
- name: AP_REDIS_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ .Values.redis.auth.externalSecret.name | quote }}
|
||||||
|
key: {{ .Values.redis.auth.externalSecret.key | quote }}
|
||||||
|
{{- else if .Values.redis.auth.password }}
|
||||||
|
- name: AP_REDIS_PASSWORD
|
||||||
|
value: {{ .Values.redis.auth.password | quote }}
|
||||||
|
{{- else if and .Values.redis.enabled .Values.redis.auth.enabled }}
|
||||||
|
- name: AP_REDIS_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ .Release.Name }}-redis
|
||||||
|
key: redis-password
|
||||||
|
{{- end }}
|
||||||
|
{{- else }}
|
||||||
|
# Memory queue (fallback)
|
||||||
|
- name: AP_REDIS_TYPE
|
||||||
|
value: "MEMORY"
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.smtp.enabled }}
|
||||||
|
# SMTP configuration
|
||||||
|
{{- if .Values.smtp.host }}
|
||||||
|
- name: AP_SMTP_HOST
|
||||||
|
value: {{ .Values.smtp.host | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.smtp.port }}
|
||||||
|
- name: AP_SMTP_PORT
|
||||||
|
value: {{ .Values.smtp.port | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.smtp.username }}
|
||||||
|
- name: AP_SMTP_USERNAME
|
||||||
|
value: {{ .Values.smtp.username | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.smtp.password }}
|
||||||
|
- name: AP_SMTP_PASSWORD
|
||||||
|
value: {{ .Values.smtp.password | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.smtp.senderEmail }}
|
||||||
|
- name: AP_SMTP_SENDER_EMAIL
|
||||||
|
value: {{ .Values.smtp.senderEmail | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.smtp.senderName }}
|
||||||
|
- name: AP_SMTP_SENDER_NAME
|
||||||
|
value: {{ .Values.smtp.senderName | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.s3.enabled }}
|
||||||
|
# S3 configuration
|
||||||
|
{{- if .Values.s3.accessKeyId }}
|
||||||
|
- name: AP_S3_ACCESS_KEY_ID
|
||||||
|
value: {{ .Values.s3.accessKeyId | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.s3.secretAccessKey }}
|
||||||
|
- name: AP_S3_SECRET_ACCESS_KEY
|
||||||
|
value: {{ .Values.s3.secretAccessKey | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.s3.bucket }}
|
||||||
|
- name: AP_S3_BUCKET
|
||||||
|
value: {{ .Values.s3.bucket | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.s3.endpoint }}
|
||||||
|
- name: AP_S3_ENDPOINT
|
||||||
|
value: {{ .Values.s3.endpoint | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.s3.region }}
|
||||||
|
- name: AP_S3_REGION
|
||||||
|
value: {{ .Values.s3.region | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.s3.useSignedUrls }}
|
||||||
|
- name: AP_S3_USE_SIGNED_URLS
|
||||||
|
value: {{ .Values.s3.useSignedUrls | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.s3.useIrsa }}
|
||||||
|
- name: AP_S3_USE_IRSA
|
||||||
|
value: {{ .Values.s3.useIrsa | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.queueUi.enabled }}
|
||||||
|
# Queue UI configuration
|
||||||
|
- name: AP_QUEUE_UI_ENABLED
|
||||||
|
value: {{ .Values.queueUi.enabled | quote }}
|
||||||
|
{{- if .Values.queueUi.username }}
|
||||||
|
- name: AP_QUEUE_UI_USERNAME
|
||||||
|
value: {{ .Values.queueUi.username | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.queueUi.password }}
|
||||||
|
- name: AP_QUEUE_UI_PASSWORD
|
||||||
|
value: {{ .Values.queueUi.password | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.engineExecutablePath }}
|
||||||
|
- name: AP_ENGINE_EXECUTABLE_PATH
|
||||||
|
value: {{ .Values.activepieces.engineExecutablePath | quote }}
|
||||||
|
{{- end }}
|
||||||
|
# OpenTelemetry configuration
|
||||||
|
{{- if .Values.activepieces.otel.enabled }}
|
||||||
|
- name: AP_OTEL_ENABLED
|
||||||
|
value: {{ .Values.activepieces.otel.enabled | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.otel.exporterOtlpEndpoint }}
|
||||||
|
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
||||||
|
value: {{ .Values.activepieces.otel.exporterOtlpEndpoint | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.activepieces.otel.exporterOtlpHeaders }}
|
||||||
|
- name: OTEL_EXPORTER_OTLP_HEADERS
|
||||||
|
value: {{ .Values.activepieces.otel.exporterOtlpHeaders | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.livenessProbe }}
|
||||||
|
livenessProbe:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.readinessProbe }}
|
||||||
|
readinessProbe:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.resources }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
volumeMounts:
|
||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
|
- name: cache
|
||||||
|
mountPath: {{ .Values.persistence.mountPath | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.volumeMounts }}
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
volumes:
|
||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
|
- name: cache
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "activepieces.fullname" . }}-cache
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.volumes }}
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{{- if .Values.autoscaling.enabled }}
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: {{ include "activepieces.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "activepieces.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: {{ include "activepieces.fullname" . }}
|
||||||
|
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||||
|
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||||
|
metrics:
|
||||||
|
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ include "activepieces.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "activepieces.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.ingress.className }}
|
||||||
|
ingressClassName: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ .host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
{{- with .pathType }}
|
||||||
|
pathType: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "activepieces.fullname" $ }}
|
||||||
|
port:
|
||||||
|
number: {{ $.Values.service.port }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ include "activepieces.fullname" . }}-cache
|
||||||
|
labels:
|
||||||
|
{{- include "activepieces.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.size | quote }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ include "activepieces.fullname" . }}-secrets
|
||||||
|
labels:
|
||||||
|
{{- include "activepieces.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
secret-generator.v1.mittwald.de/autogenerate: encryption-key
|
||||||
|
secret-generator.v1.mittwald.de/encoding: hex
|
||||||
|
secret-generator.v1.mittwald.de/length: "32"
|
||||||
|
type: Opaque
|
||||||
|
data: {}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ include "activepieces.fullname" . }}-jwt-secret
|
||||||
|
labels:
|
||||||
|
{{- include "activepieces.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
secret-generator.v1.mittwald.de/autogenerate: jwt-secret
|
||||||
|
secret-generator.v1.mittwald.de/length: "64"
|
||||||
|
type: Opaque
|
||||||
|
data: {}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "activepieces.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "activepieces.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
{{- include "activepieces.selectorLabels" . | nindent 4 }}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{{- if .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "activepieces.serviceAccountName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "activepieces.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: "{{ include "activepieces.fullname" . }}-test-connection"
|
||||||
|
labels:
|
||||||
|
{{- include "activepieces.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
"helm.sh/hook": test
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: wget
|
||||||
|
image: busybox
|
||||||
|
command: ['wget']
|
||||||
|
args: ['{{ include "activepieces.fullname" . }}:{{ .Values.service.port }}']
|
||||||
|
restartPolicy: Never
|
||||||
324
activepieces-fork/deploy/activepieces-helm/values.yaml
Normal file
324
activepieces-fork/deploy/activepieces-helm/values.yaml
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
# Default values for activepieces.
|
||||||
|
# This is a YAML-formatted file.
|
||||||
|
# Declare variables to be passed into your templates.
|
||||||
|
|
||||||
|
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/activepieces/activepieces
|
||||||
|
# This sets the pull policy for images.
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
# Overrides the image tag whose default is the chart appVersion.
|
||||||
|
tag: ""
|
||||||
|
|
||||||
|
# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||||
|
imagePullSecrets: []
|
||||||
|
# This is to override the chart name.
|
||||||
|
nameOverride: ""
|
||||||
|
fullnameOverride: ""
|
||||||
|
|
||||||
|
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||||
|
serviceAccount:
|
||||||
|
# Specifies whether a service account should be created
|
||||||
|
create: true
|
||||||
|
# Automatically mount a ServiceAccount's API credentials?
|
||||||
|
automount: true
|
||||||
|
# Annotations to add to the service account
|
||||||
|
annotations: {}
|
||||||
|
# The name of the service account to use.
|
||||||
|
# If not set and create is true, a name is generated using the fullname template
|
||||||
|
name: ""
|
||||||
|
|
||||||
|
# This is for setting Kubernetes Annotations to a Pod.
|
||||||
|
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||||
|
podAnnotations: {}
|
||||||
|
# This is for setting Kubernetes Labels to a Pod.
|
||||||
|
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||||
|
podLabels: {}
|
||||||
|
|
||||||
|
podSecurityContext: {}
|
||||||
|
# fsGroup: 2000
|
||||||
|
|
||||||
|
securityContext: {}
|
||||||
|
# capabilities:
|
||||||
|
# drop:
|
||||||
|
# - ALL
|
||||||
|
# readOnlyRootFilesystem: true
|
||||||
|
# runAsNonRoot: true
|
||||||
|
# runAsUser: 1000
|
||||||
|
|
||||||
|
# Container configuration
|
||||||
|
container:
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||||
|
service:
|
||||||
|
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
|
||||||
|
type: ClusterIP
|
||||||
|
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
className: ""
|
||||||
|
annotations: {}
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
hosts:
|
||||||
|
- host: chart-example.local
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
tls: []
|
||||||
|
# - secretName: chart-example-tls
|
||||||
|
# hosts:
|
||||||
|
# - chart-example.local
|
||||||
|
|
||||||
|
resources: {}
|
||||||
|
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||||
|
# choice for the user. This also increases chances charts run on environments with little
|
||||||
|
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||||
|
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||||
|
# limits:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
# requests:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
|
||||||
|
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /v1/health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /v1/health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
|
||||||
|
# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
|
||||||
|
autoscaling:
|
||||||
|
enabled: false
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 100
|
||||||
|
targetCPUUtilizationPercentage: 80
|
||||||
|
# targetMemoryUtilizationPercentage: 80
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: 2Gi
|
||||||
|
mountPath: "/usr/src/app/cache"
|
||||||
|
|
||||||
|
volumes: []
|
||||||
|
volumeMounts: []
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
tolerations: []
|
||||||
|
affinity: {}
|
||||||
|
|
||||||
|
activepieces:
|
||||||
|
# ============================================================================
|
||||||
|
# REQUIRED: Core configuration that must be set for production deployment
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# The URL where Activepieces will be accessible (e.g., https://activepieces.yourdomain.com)
|
||||||
|
frontendUrl: "http://localhost:4200"
|
||||||
|
|
||||||
|
# Edition: ce (Community) or ee (Enterprise)
|
||||||
|
# Community Edition (ce) is the default open-source version
|
||||||
|
edition: "ce"
|
||||||
|
|
||||||
|
# Execution mode for flows - see: https://www.activepieces.com/docs/install/architecture/workers
|
||||||
|
# Options: SANDBOX_CODE_ONLY, UNSANDBOXED
|
||||||
|
executionMode: "SANDBOX_CODE_ONLY"
|
||||||
|
|
||||||
|
# Environment: prod or dev
|
||||||
|
environment: "prod"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# OPTIONAL: Additional configuration (defaults are usually sufficient)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
telemetryEnabled: true
|
||||||
|
templatesSourceUrl: "https://cloud.activepieces.com/api/v1/templates"
|
||||||
|
flowWorkerConcurrency: ""
|
||||||
|
scheduledWorkerConcurrency: ""
|
||||||
|
triggerDefaultPollInterval: ""
|
||||||
|
triggerTimeoutSeconds: ""
|
||||||
|
flowTimeoutSeconds: ""
|
||||||
|
webhookTimeoutSeconds: ""
|
||||||
|
executionDataRetentionDays: ""
|
||||||
|
issueArchiveDays: ""
|
||||||
|
pausedFlowTimeoutDays: ""
|
||||||
|
logLevel: "info"
|
||||||
|
logPretty: false
|
||||||
|
apiRateLimiting:
|
||||||
|
authn:
|
||||||
|
enabled: false
|
||||||
|
max: 100
|
||||||
|
window: 60
|
||||||
|
projectRateLimiter:
|
||||||
|
enabled: false
|
||||||
|
piecesSource: ""
|
||||||
|
devPieces: false
|
||||||
|
piecesSyncMode: ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# OPTIONAL: Advanced configuration (only set if needed)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Networking & API
|
||||||
|
clientRealIpHeader: "" # Header to extract real client IP (e.g., X-Forwarded-For)
|
||||||
|
apiKey: "" # API key for platform authentication
|
||||||
|
internalUrl: "" # Internal URL for service communication
|
||||||
|
|
||||||
|
# Performance & Limits
|
||||||
|
maxConcurrentJobsPerProject: "" # Limit concurrent jobs per project
|
||||||
|
maxFileSizeMb: "" # Maximum file upload size in MB
|
||||||
|
sandboxMemoryLimit: "" # Memory limit for sandbox execution
|
||||||
|
sandboxPropagatedEnvVars: "" # Environment variables to pass to sandbox
|
||||||
|
|
||||||
|
# Integrations
|
||||||
|
perplexityBaseUrl: "" # Custom Perplexity AI base URL
|
||||||
|
featurebaseApiKey: "" # Featurebase API key for feedback
|
||||||
|
appWebhookSecrets: "" # Secrets for app webhooks
|
||||||
|
|
||||||
|
# UI & Behavior
|
||||||
|
showChangelog: true # Show changelog to users
|
||||||
|
enableFlowOnPublish: true # Auto-enable flows when published
|
||||||
|
|
||||||
|
# System Paths
|
||||||
|
configPath: "" # Custom config file path
|
||||||
|
engineExecutablePath: "dist/packages/engine/main.js" # Engine executable path
|
||||||
|
|
||||||
|
# OpenTelemetry Configuration (for observability)
|
||||||
|
otel:
|
||||||
|
enabled: false
|
||||||
|
exporterOtlpEndpoint: ""
|
||||||
|
exporterOtlpHeaders: ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Database Configuration
|
||||||
|
# PostgreSQL is deployed by default using the Bitnami subchart
|
||||||
|
# ============================================================================
|
||||||
|
postgresql:
|
||||||
|
# Set to true to deploy PostgreSQL subchart (Bitnami)
|
||||||
|
# Set to false and provide host/url to use external PostgreSQL
|
||||||
|
enabled: true
|
||||||
|
# External PostgreSQL host (required if enabled=false and using external PostgreSQL)
|
||||||
|
host: ""
|
||||||
|
# PostgreSQL port
|
||||||
|
port: 5432
|
||||||
|
# Enable SSL for PostgreSQL connection
|
||||||
|
useSSL: false
|
||||||
|
# PostgreSQL connection URL (alternative to host/port/auth)
|
||||||
|
# If provided, host/port/auth will be ignored
|
||||||
|
url: ""
|
||||||
|
# SSL CA certificate for PostgreSQL (if useSSL is true)
|
||||||
|
sslCa: ""
|
||||||
|
auth:
|
||||||
|
database: "activepieces"
|
||||||
|
username: "postgres"
|
||||||
|
# Password for PostgreSQL
|
||||||
|
# - If using subchart (enabled=true), password is automatically generated if not provided
|
||||||
|
# - If using external PostgreSQL, provide password here or use externalSecret
|
||||||
|
password: ""
|
||||||
|
# External secret reference for password (alternative to password field)
|
||||||
|
# Use this when password is stored in a Kubernetes secret (e.g., External Secrets Operator)
|
||||||
|
# externalSecret:
|
||||||
|
# name: "postgresql-credentials"
|
||||||
|
# key: "password"
|
||||||
|
primary:
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Queue Configuration
|
||||||
|
# Redis is deployed by default using the Bitnami subchart
|
||||||
|
# ============================================================================
|
||||||
|
redis:
|
||||||
|
# Set to true to deploy Redis subchart (Bitnami)
|
||||||
|
# Set to false and provide host/url to use external Redis
|
||||||
|
enabled: true
|
||||||
|
# External Redis host (required if enabled=false and using external Redis)
|
||||||
|
host: ""
|
||||||
|
# Redis port
|
||||||
|
port: 6379
|
||||||
|
# Enable SSL for Redis connection
|
||||||
|
useSSL: false
|
||||||
|
# SSL CA certificate file path for Redis (if useSSL is true)
|
||||||
|
sslCaFile: ""
|
||||||
|
# Redis database number
|
||||||
|
db: 0
|
||||||
|
# Redis connection URL (alternative to host/port/auth)
|
||||||
|
# If provided, host/port/auth will be ignored
|
||||||
|
url: ""
|
||||||
|
# Redis type: "standalone" or "sentinel"
|
||||||
|
type: "standalone"
|
||||||
|
# Redis username (for Redis 6+ ACL)
|
||||||
|
user: ""
|
||||||
|
sentinel:
|
||||||
|
name: ""
|
||||||
|
hosts: ""
|
||||||
|
role: ""
|
||||||
|
failedJob:
|
||||||
|
retentionDays: 7
|
||||||
|
retentionMaxCount: 100
|
||||||
|
auth:
|
||||||
|
# Set to true if Redis requires authentication
|
||||||
|
# - If using subchart (enabled=true), password is automatically generated if not provided
|
||||||
|
# - If using external Redis, provide password or use externalSecret
|
||||||
|
enabled: true
|
||||||
|
# Password for Redis
|
||||||
|
password: ""
|
||||||
|
# External secret reference for password (alternative to password field)
|
||||||
|
# Use this when password is stored in a Kubernetes secret (e.g., External Secrets Operator)
|
||||||
|
# externalSecret:
|
||||||
|
# name: "redis-credentials"
|
||||||
|
# key: "password"
|
||||||
|
master:
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# OPTIONAL: Email Configuration
|
||||||
|
# Enable SMTP to send email notifications
|
||||||
|
# ============================================================================
|
||||||
|
smtp:
|
||||||
|
enabled: false # Set to true and configure below to enable emails
|
||||||
|
host: ""
|
||||||
|
port: 587
|
||||||
|
username: ""
|
||||||
|
password: ""
|
||||||
|
senderEmail: ""
|
||||||
|
senderName: ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# OPTIONAL: S3 Storage Configuration
|
||||||
|
# Enable S3 for file storage (alternative to local storage)
|
||||||
|
# ============================================================================
|
||||||
|
s3:
|
||||||
|
enabled: false # Set to true and configure below to use S3
|
||||||
|
accessKeyId: ""
|
||||||
|
secretAccessKey: ""
|
||||||
|
bucket: ""
|
||||||
|
endpoint: "" # For S3-compatible services like MinIO
|
||||||
|
region: ""
|
||||||
|
useSignedUrls: false
|
||||||
|
useIrsa: false # Use IAM Roles for Service Accounts (EKS)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# OPTIONAL: Queue UI Configuration
|
||||||
|
# Enable BullMQ Board for monitoring job queues
|
||||||
|
# ============================================================================
|
||||||
|
queueUi:
|
||||||
|
enabled: false # Set to true and configure credentials below
|
||||||
|
username: ""
|
||||||
|
password: ""
|
||||||
33
activepieces-fork/deploy/pulumi/.gitignore
vendored
Normal file
33
activepieces-fork/deploy/pulumi/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/.pulumi/
|
||||||
|
/.vscode/
|
||||||
|
/.vs/
|
||||||
|
bin/
|
||||||
|
build/
|
||||||
|
node_modules/
|
||||||
|
*.pyc
|
||||||
|
.Python
|
||||||
|
venv/
|
||||||
|
include/
|
||||||
|
lib/
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
# https://www.pulumi.com/blog/iac-recommended-practices-developer-stacks-git-branches/#using-developer-stacks
|
||||||
|
# Pulumi.*.yaml
|
||||||
|
# Pulumi.*dev*.yaml
|
||||||
|
.idea/
|
||||||
|
.ionide/
|
||||||
|
*.iml
|
||||||
|
key.rsa*
|
||||||
|
obj/
|
||||||
|
vendor
|
||||||
|
Gopkg.lock
|
||||||
|
**/.DS_Store
|
||||||
|
|
||||||
|
**/ci-scripts
|
||||||
|
|
||||||
|
# Java app
|
||||||
|
.gradle/
|
||||||
|
.settings/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
target/
|
||||||
22
activepieces-fork/deploy/pulumi/Pulumi.activepieces-dev.yaml
Normal file
22
activepieces-fork/deploy/pulumi/Pulumi.activepieces-dev.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
encryptionsalt: v1:wHCVNl3bj/g=:v1:mxogi9ZeBjIcxNZC:q+bjpLv9rnJnu8qq7xwKGLd/GAZOqA==
|
||||||
|
config:
|
||||||
|
activepieces:environment: "dev"
|
||||||
|
activepieces:apEncryptionKey:
|
||||||
|
activepieces:apJwtSecret:
|
||||||
|
activepieces:deployLocalBuild: "false"
|
||||||
|
activepieces:repoName:
|
||||||
|
activepieces:containerCpu: "256"
|
||||||
|
activepieces:containerMemory: "512"
|
||||||
|
activepieces:containerInstances: "1"
|
||||||
|
activepieces:usePostgres: "false"
|
||||||
|
activepieces:dbInstanceClass: "db.t3.small"
|
||||||
|
activepieces:dbUsername: "postgres"
|
||||||
|
activepieces:dbIsPublic: "false"
|
||||||
|
activepieces:dbPassword:
|
||||||
|
secure: v1:MXNSOcqZCp10X2PX:mU2iTrcETjdisk8FkD5yHLJYUxRei/9l
|
||||||
|
activepieces:addIpToPostgresSecurityGroup:
|
||||||
|
activepieces:useRedis: "false"
|
||||||
|
activepieces:redisNodeType: "cache.t3.small"
|
||||||
|
activepieces:domain:
|
||||||
|
activepieces:subDomain:
|
||||||
|
aws:region: "us-east-1"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
encryptionsalt: v1:icXg2cmIvSc=:v1:y8+4YhdMCPPDY26J:5cNYmimH353n8sjUDDc6srvcPgb+8Q==
|
||||||
|
config:
|
||||||
|
activepieces:environment: "prod"
|
||||||
|
activepieces:apEncryptionKey:
|
||||||
|
activepieces:apJwtSecret:
|
||||||
|
activepieces:deployLocalBuild: "true"
|
||||||
|
activepieces:repoName: "activepieces-prod-repo"
|
||||||
|
activepieces:containerCpu: "512"
|
||||||
|
activepieces:containerMemory: "1024"
|
||||||
|
activepieces:containerInstances: "1"
|
||||||
|
activepieces:usePostgres: "true"
|
||||||
|
activepieces:dbInstanceClass: "db.t3.small"
|
||||||
|
activepieces:dbIsPublic: "false"
|
||||||
|
activepieces:dbPassword:
|
||||||
|
secure: v1:MXNSOcqZCp10X2PX:mU2iTrcETjdisk8FkD5yHLJYUxRei/9l
|
||||||
|
activepieces:dbUsername: "postgres"
|
||||||
|
activepieces:useRedis: "true"
|
||||||
|
activepieces:redisNodeType: "cache.t3.small"
|
||||||
|
aws:region: "us-east-1"
|
||||||
56
activepieces-fork/deploy/pulumi/Pulumi.yaml
Normal file
56
activepieces-fork/deploy/pulumi/Pulumi.yaml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
runtime: nodejs
|
||||||
|
name: activepieces
|
||||||
|
description: A Pulumi template to deploy Activepieces in a development or production configuration.
|
||||||
|
stack: activepieces-dev
|
||||||
|
template:
|
||||||
|
description: Deploy Activepieces into into an ECS Fargate instance & optionally add Postgres, Redis and a DNS registration with SSL.
|
||||||
|
config:
|
||||||
|
aws:region:
|
||||||
|
description: The AWS region to deploy into
|
||||||
|
default: us-west-2
|
||||||
|
environment:
|
||||||
|
description: Environment
|
||||||
|
default: prod
|
||||||
|
containerCpu:
|
||||||
|
description: The amount of CPU to allocate for the container
|
||||||
|
default: 256
|
||||||
|
containerMemory:
|
||||||
|
description: The amount of memory to allocate for the container
|
||||||
|
default: 512
|
||||||
|
containerInstances:
|
||||||
|
description: Number of running containers behind load balancer
|
||||||
|
default: 1
|
||||||
|
usePostgres:
|
||||||
|
description: Add Postgres for storage or use SQLite3 locally
|
||||||
|
default: true
|
||||||
|
dbIsPublic:
|
||||||
|
description: Should Db be publicly reachable. Ignored if usePostgres is false.
|
||||||
|
default: false
|
||||||
|
dbUsername:
|
||||||
|
description: Default username for the Postgres. Ignored if usePostgres is false
|
||||||
|
default: postgres
|
||||||
|
dbPassword:
|
||||||
|
description: Defaults to "postgres". Ignored if usePostgres is false
|
||||||
|
default: postgres
|
||||||
|
secret: true
|
||||||
|
dbInstanceClass:
|
||||||
|
description: The size of the RDS instance
|
||||||
|
default: db.t3.micro
|
||||||
|
useRedis:
|
||||||
|
description: Use a single node Redis cluster or in-memory
|
||||||
|
default: true
|
||||||
|
redisNodeType:
|
||||||
|
description: Node type for the Redis 7 cluster
|
||||||
|
default: cache.t3.micro
|
||||||
|
domain:
|
||||||
|
description: Optional - E.g. "yourdomain.com". Hosted zone must already exist in Route 53. Creates SSL cert
|
||||||
|
subDomain:
|
||||||
|
description: Optional - E.g. "activepieces". "domain" must be set
|
||||||
|
addIpToPostgresSecurityGroup:
|
||||||
|
description: Optional - An IP address to add to the allowed inbound traffic for the Postgres
|
||||||
|
apEncryptionKey:
|
||||||
|
description: Optional - Run 'openssl rand -hex 16' locally to generate or leave blank to auto-generate
|
||||||
|
secret: true
|
||||||
|
apJwtSecret:
|
||||||
|
description: Optional - Run 'openssl rand -hex 32' locally to generate or leave blank to auto-generate
|
||||||
|
secret: true
|
||||||
3
activepieces-fork/deploy/pulumi/README.md
Normal file
3
activepieces-fork/deploy/pulumi/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
||||||
|
See instruction on https://www.activepieces.com/docs/install/options/aws
|
||||||
16
activepieces-fork/deploy/pulumi/autotag.ts
Normal file
16
activepieces-fork/deploy/pulumi/autotag.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as pulumi from "@pulumi/pulumi";
|
||||||
|
import { isTaggable } from "./taggable";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* registerAutoTags registers a global stack transformation that merges a set
|
||||||
|
* of tags with whatever was also explicitly added to the resource definition.
|
||||||
|
*/
|
||||||
|
export function registerAutoTags(autoTags: Record<string, string>): void {
|
||||||
|
pulumi.runtime.registerStackTransformation((args) => {
|
||||||
|
if (isTaggable(args.type)) {
|
||||||
|
args.props["tags"] = { ...args.props["tags"], ...autoTags };
|
||||||
|
return { props: args.props, opts: args.opts };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
466
activepieces-fork/deploy/pulumi/index.ts
Normal file
466
activepieces-fork/deploy/pulumi/index.ts
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
import * as aws from "@pulumi/aws";
|
||||||
|
import * as docker from "@pulumi/docker";
|
||||||
|
import * as pulumi from "@pulumi/pulumi";
|
||||||
|
import * as awsx from "@pulumi/awsx";
|
||||||
|
import { ApplicationLoadBalancer } from "@pulumi/awsx/lb/applicationLoadBalancer";
|
||||||
|
import { registerAutoTags } from './autotag';
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
|
||||||
|
const stack = pulumi.getStack();
|
||||||
|
const config = new pulumi.Config();
|
||||||
|
|
||||||
|
const apEncryptionKey = config.getSecret("apEncryptionKey")?.apply(secretValue => {
|
||||||
|
return secretValue || child_process.execSync("openssl rand -hex 16").toString().trim();
|
||||||
|
});
|
||||||
|
const apJwtSecret = config.getSecret("apJwtSecret")?.apply(secretValue => {
|
||||||
|
return secretValue || child_process.execSync("openssl rand -hex 32").toString().trim();
|
||||||
|
});
|
||||||
|
const containerCpu = config.requireNumber("containerCpu");
|
||||||
|
const containerMemory = config.requireNumber("containerMemory");
|
||||||
|
const containerInstances = config.requireNumber("containerInstances");
|
||||||
|
const addIpToPostgresSecurityGroup = config.get("addIpToPostgresSecurityGroup");
|
||||||
|
const domain = config.get("domain");
|
||||||
|
const subDomain = config.get("subDomain");
|
||||||
|
const usePostgres = config.requireBoolean("usePostgres");
|
||||||
|
const useRedis = config.requireBoolean("useRedis");
|
||||||
|
const redisNodeType = config.require("redisNodeType");
|
||||||
|
const dbIsPublic = config.getBoolean("dbIsPublic");
|
||||||
|
const dbUsername = config.get("dbUsername");
|
||||||
|
const dbPassword = config.getSecret("dbPassword");
|
||||||
|
const dbInstanceClass = config.require("dbInstanceClass");
|
||||||
|
|
||||||
|
// Add tags for every resource that allows them, with the following properties.
|
||||||
|
// Useful to know who or what created the resource/service
|
||||||
|
registerAutoTags({
|
||||||
|
"pulumi:Project": pulumi.getProject(),
|
||||||
|
"pulumi:Stack": pulumi.getStack(),
|
||||||
|
"Created by": config.get("author") || child_process.execSync("pulumi whoami").toString().trim().replace('\\', '/')
|
||||||
|
});
|
||||||
|
|
||||||
|
let imageName;
|
||||||
|
|
||||||
|
// Check if we're deploying a local build or direct from Docker Hub
|
||||||
|
if (config.getBoolean("deployLocalBuild")) {
|
||||||
|
|
||||||
|
const repoName = config.require("repoName");
|
||||||
|
|
||||||
|
const repo = new aws.ecr.Repository(repoName, {
|
||||||
|
name: repoName // https://www.pulumi.com/docs/intro/concepts/resources/names/#autonaming
|
||||||
|
}); // Create a private ECR repository
|
||||||
|
|
||||||
|
const repoUrl = pulumi.interpolate`${repo.repositoryUrl}`; // Get registry info (creds and endpoint)
|
||||||
|
const name = pulumi.interpolate`${repoUrl}:latest`;
|
||||||
|
|
||||||
|
// Get the repository credentials we use to push the image to the repository
|
||||||
|
const repoCreds = repo.registryId.apply(async (registryId) => {
|
||||||
|
const credentials = await aws.ecr.getCredentials({
|
||||||
|
registryId: registryId,
|
||||||
|
});
|
||||||
|
const decodedCredentials = Buffer.from(credentials.authorizationToken, "base64").toString();
|
||||||
|
const [username, password] = decodedCredentials.split(":");
|
||||||
|
return {
|
||||||
|
server: credentials.proxyEndpoint,
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build and publish the container image.
|
||||||
|
const image = new docker.Image(stack, {
|
||||||
|
build: {
|
||||||
|
context: `../../`,
|
||||||
|
dockerfile: `../../Dockerfile`,
|
||||||
|
builderVersion: "BuilderBuildKit",
|
||||||
|
args: {
|
||||||
|
"BUILDKIT_INLINE_CACHE": "1"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
skipPush: pulumi.runtime.isDryRun(),
|
||||||
|
imageName: name,
|
||||||
|
registry: repoCreds
|
||||||
|
});
|
||||||
|
|
||||||
|
imageName = image.imageName;
|
||||||
|
|
||||||
|
pulumi.log.info(`Finished pushing image to ECR`, image);
|
||||||
|
} else {
|
||||||
|
imageName = process.env.IMAGE_NAME || config.get("imageName") || "activepieces/activepieces:latest";
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerEnvironmentVars: awsx.types.input.ecs.TaskDefinitionKeyValuePairArgs[] = [];
|
||||||
|
|
||||||
|
// Allocate a new VPC with the default settings:
|
||||||
|
const vpc = new awsx.ec2.Vpc(`${stack}-vpc`, {
|
||||||
|
numberOfAvailabilityZones: 2,
|
||||||
|
natGateways: {
|
||||||
|
strategy: "Single"
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
// For some reason, this is how you name a VPC with AWS:
|
||||||
|
// https://github.com/pulumi/pulumi-terraform/issues/38#issue-262186406
|
||||||
|
Name: `${stack}-vpc`
|
||||||
|
},
|
||||||
|
enableDnsHostnames: true,
|
||||||
|
enableDnsSupport: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const albSecGroup = new aws.ec2.SecurityGroup(`${stack}-alb-sg`, {
|
||||||
|
name: `${stack}-alb-sg`,
|
||||||
|
vpcId: vpc.vpcId,
|
||||||
|
ingress: [{ // Allow only http & https traffic
|
||||||
|
protocol: "tcp",
|
||||||
|
fromPort: 443,
|
||||||
|
toPort: 443,
|
||||||
|
cidrBlocks: ["0.0.0.0/0"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "tcp",
|
||||||
|
fromPort: 80,
|
||||||
|
toPort: 80,
|
||||||
|
cidrBlocks: ["0.0.0.0/0"]
|
||||||
|
}],
|
||||||
|
egress: [{
|
||||||
|
protocol: "-1",
|
||||||
|
fromPort: 0,
|
||||||
|
toPort: 0,
|
||||||
|
cidrBlocks: ["0.0.0.0/0"]
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
const fargateSecGroup = new aws.ec2.SecurityGroup(`${stack}-fargate-sg`, {
|
||||||
|
name: `${stack}-fargate-sg`,
|
||||||
|
vpcId: vpc.vpcId,
|
||||||
|
ingress: [
|
||||||
|
{
|
||||||
|
protocol: "tcp",
|
||||||
|
fromPort: 80,
|
||||||
|
toPort: 80,
|
||||||
|
securityGroups: [albSecGroup.id]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
egress: [ // allow all outbound traffic
|
||||||
|
{
|
||||||
|
protocol: "-1",
|
||||||
|
fromPort: 0,
|
||||||
|
toPort: 0,
|
||||||
|
cidrBlocks: ["0.0.0.0/0"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (usePostgres) {
|
||||||
|
const rdsSecurityGroupArgs: aws.ec2.SecurityGroupArgs = {
|
||||||
|
name: `${stack}-db-sg`,
|
||||||
|
vpcId: vpc.vpcId,
|
||||||
|
ingress: [{
|
||||||
|
protocol: "tcp",
|
||||||
|
fromPort: 5432,
|
||||||
|
toPort: 5432,
|
||||||
|
securityGroups: [fargateSecGroup.id] // The id of the Fargate security group
|
||||||
|
}],
|
||||||
|
egress: [ // allow all outbound traffic
|
||||||
|
{
|
||||||
|
protocol: "-1",
|
||||||
|
fromPort: 0,
|
||||||
|
toPort: 0,
|
||||||
|
cidrBlocks: ["0.0.0.0/0"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optionally add the current outgoing public IP address to the CIDR block
|
||||||
|
// so that they can connect directly to the Db during development
|
||||||
|
if (addIpToPostgresSecurityGroup) {
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
rdsSecurityGroupArgs.ingress.push({
|
||||||
|
protocol: "tcp",
|
||||||
|
fromPort: 5432,
|
||||||
|
toPort: 5432,
|
||||||
|
cidrBlocks: [`${addIpToPostgresSecurityGroup}/32`],
|
||||||
|
description: `Public IP for local connection`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rdsSecurityGroup = new aws.ec2.SecurityGroup(`${stack}-db-sg`, rdsSecurityGroupArgs);
|
||||||
|
|
||||||
|
const rdsSubnets = new aws.rds.SubnetGroup(`${stack}-db-subnet-group`, {
|
||||||
|
name: `${stack}-db-subnet-group`,
|
||||||
|
subnetIds: dbIsPublic ? vpc.publicSubnetIds : vpc.privateSubnetIds
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = new aws.rds.Instance(stack, {
|
||||||
|
allocatedStorage: 10,
|
||||||
|
engine: "postgres",
|
||||||
|
engineVersion: "14.9",
|
||||||
|
identifier: stack, // In RDS
|
||||||
|
dbName: "postgres", // When connected to the DB host
|
||||||
|
instanceClass: dbInstanceClass,
|
||||||
|
port: 5432,
|
||||||
|
publiclyAccessible: dbIsPublic,
|
||||||
|
skipFinalSnapshot: true,
|
||||||
|
storageType: "gp2",
|
||||||
|
username: dbUsername,
|
||||||
|
password: dbPassword,
|
||||||
|
dbSubnetGroupName: rdsSubnets.id,
|
||||||
|
vpcSecurityGroupIds: [rdsSecurityGroup.id],
|
||||||
|
backupRetentionPeriod: 0,
|
||||||
|
applyImmediately: true,
|
||||||
|
allowMajorVersionUpgrade: true,
|
||||||
|
autoMinorVersionUpgrade: true
|
||||||
|
}, {
|
||||||
|
protect: dbIsPublic === false,
|
||||||
|
deleteBeforeReplace: true
|
||||||
|
});
|
||||||
|
|
||||||
|
containerEnvironmentVars.push(
|
||||||
|
{
|
||||||
|
name: "AP_POSTGRES_DATABASE",
|
||||||
|
value: db.dbName
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_POSTGRES_HOST",
|
||||||
|
value: db.address
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_POSTGRES_PORT",
|
||||||
|
value: pulumi.interpolate`${db.port}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_POSTGRES_USERNAME",
|
||||||
|
value: db.username
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_POSTGRES_PASSWORD",
|
||||||
|
value: config.requireSecret("dbPassword")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_POSTGRES_USE_SSL",
|
||||||
|
value: "false"
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
containerEnvironmentVars.push(
|
||||||
|
{
|
||||||
|
name: "AP_DB_TYPE",
|
||||||
|
value: "SQLITE3"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useRedis) {
|
||||||
|
|
||||||
|
const redisCluster = new aws.elasticache.Cluster(`${stack}-redis-cluster`, {
|
||||||
|
clusterId: `${stack}-redis-cluster`,
|
||||||
|
engine: "redis",
|
||||||
|
engineVersion: '7.0',
|
||||||
|
nodeType: redisNodeType,
|
||||||
|
numCacheNodes: 1,
|
||||||
|
parameterGroupName: "default.redis7",
|
||||||
|
port: 6379,
|
||||||
|
subnetGroupName: new aws.elasticache.SubnetGroup(`${stack}-redis-subnet-group`, {
|
||||||
|
name: `${stack}-redis-subnet-group`,
|
||||||
|
subnetIds: vpc.privateSubnetIds
|
||||||
|
}).id,
|
||||||
|
securityGroupIds: [
|
||||||
|
new aws.ec2.SecurityGroup(`${stack}-redis-sg`, {
|
||||||
|
name: `${stack}-redis-sg`,
|
||||||
|
vpcId: vpc.vpcId,
|
||||||
|
ingress: [{
|
||||||
|
protocol: "tcp",
|
||||||
|
fromPort: 6379, // The standard port for Redis
|
||||||
|
toPort: 6379,
|
||||||
|
securityGroups: [fargateSecGroup.id]
|
||||||
|
}],
|
||||||
|
egress: [{
|
||||||
|
protocol: "-1",
|
||||||
|
fromPort: 0,
|
||||||
|
toPort: 0,
|
||||||
|
cidrBlocks: ["0.0.0.0/0"]
|
||||||
|
}]
|
||||||
|
}).id
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const redisUrl = pulumi.interpolate`${redisCluster.cacheNodes[0].address}:${redisCluster.cacheNodes[0].port}`;
|
||||||
|
containerEnvironmentVars.push(
|
||||||
|
{
|
||||||
|
name: "AP_REDIS_URL",
|
||||||
|
value: redisUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
containerEnvironmentVars.push(
|
||||||
|
{
|
||||||
|
name: "AP_QUEUE_MODE",
|
||||||
|
value: "MEMORY"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let alb: ApplicationLoadBalancer;
|
||||||
|
// Export the URL so we can easily access it.
|
||||||
|
let frontendUrl;
|
||||||
|
|
||||||
|
if (subDomain && domain) {
|
||||||
|
const fullDomain = `${subDomain}.${domain}`;
|
||||||
|
|
||||||
|
const exampleCertificate = new aws.acm.Certificate(`${stack}-cert`, {
|
||||||
|
domainName: fullDomain,
|
||||||
|
validationMethod: "DNS",
|
||||||
|
});
|
||||||
|
|
||||||
|
const hostedZoneId = aws.route53.getZone({ name: domain }, { async: true }).then(zone => zone.zoneId);
|
||||||
|
|
||||||
|
// DNS records to verify SSL Certificate
|
||||||
|
const certificateValidationDomain = new aws.route53.Record(`${fullDomain}-validation`, {
|
||||||
|
name: exampleCertificate.domainValidationOptions[0].resourceRecordName,
|
||||||
|
zoneId: hostedZoneId,
|
||||||
|
type: exampleCertificate.domainValidationOptions[0].resourceRecordType,
|
||||||
|
records: [exampleCertificate.domainValidationOptions[0].resourceRecordValue],
|
||||||
|
ttl: 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateValidation = new aws.acm.CertificateValidation(`${fullDomain}-cert-validation`, {
|
||||||
|
certificateArn: exampleCertificate.arn,
|
||||||
|
validationRecordFqdns: [certificateValidationDomain.fqdn],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Creates an ALB associated with our custom VPC.
|
||||||
|
alb = new awsx.lb.ApplicationLoadBalancer(`${stack}-alb`, {
|
||||||
|
securityGroups: [albSecGroup.id],
|
||||||
|
name: `${stack}-alb`,
|
||||||
|
subnetIds: vpc.publicSubnetIds,
|
||||||
|
listeners: [{
|
||||||
|
port: 80, // port on the docker container
|
||||||
|
protocol: "HTTP",
|
||||||
|
defaultActions: [{
|
||||||
|
type: "redirect",
|
||||||
|
redirect: {
|
||||||
|
protocol: "HTTPS",
|
||||||
|
port: "443",
|
||||||
|
statusCode: "HTTP_301",
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "HTTPS",
|
||||||
|
port: 443,
|
||||||
|
certificateArn: certificateValidation.certificateArn
|
||||||
|
}],
|
||||||
|
defaultTargetGroup: {
|
||||||
|
name: `${stack}-alb-tg`,
|
||||||
|
port: 80 // port on the docker container ,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a DNS record for the load balancer
|
||||||
|
const albDomain = new aws.route53.Record(fullDomain, {
|
||||||
|
name: fullDomain,
|
||||||
|
zoneId: hostedZoneId,
|
||||||
|
type: "CNAME",
|
||||||
|
records: [alb.loadBalancer.dnsName],
|
||||||
|
ttl: 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
frontendUrl = pulumi.interpolate`https://${subDomain}.${domain}`;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Creates an ALB associated with our custom VPC.
|
||||||
|
alb = new awsx.lb.ApplicationLoadBalancer(`${stack}-alb`, {
|
||||||
|
securityGroups: [albSecGroup.id],
|
||||||
|
name: `${stack}-alb`,
|
||||||
|
subnetIds: vpc.publicSubnetIds,
|
||||||
|
listeners: [{
|
||||||
|
port: 80, // exposed port from the docker file
|
||||||
|
protocol: "HTTP"
|
||||||
|
}],
|
||||||
|
defaultTargetGroup: {
|
||||||
|
name: `${stack}-alb-tg`,
|
||||||
|
port: 80, // port on the docker container
|
||||||
|
protocol: "HTTP"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frontendUrl = pulumi.interpolate`http://${alb.loadBalancer.dnsName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const environmentVariables = [
|
||||||
|
...containerEnvironmentVars,
|
||||||
|
{
|
||||||
|
name: "AP_ENGINE_EXECUTABLE_PATH",
|
||||||
|
value: "dist/packages/engine/main.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_ENCRYPTION_KEY",
|
||||||
|
value: apEncryptionKey
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_JWT_SECRET",
|
||||||
|
value: apJwtSecret
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_ENVIRONMENT",
|
||||||
|
value: "prod"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_FRONTEND_URL",
|
||||||
|
value: frontendUrl
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_TRIGGER_DEFAULT_POLL_INTERVAL",
|
||||||
|
value: "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_EXECUTION_MODE",
|
||||||
|
value: "UNSANDBOXED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_REDIS_USE_SSL",
|
||||||
|
value: "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_SANDBOX_RUN_TIME_SECONDS",
|
||||||
|
value: "600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_TELEMETRY_ENABLED",
|
||||||
|
value: "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AP_TEMPLATES_SOURCE_URL",
|
||||||
|
value: "https://cloud.activepieces.com/api/v1/templates"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const fargateService = new awsx.ecs.FargateService(`${stack}-fg`, {
|
||||||
|
name: `${stack}-fg`,
|
||||||
|
cluster: (new aws.ecs.Cluster(`${stack}-cluster`, {
|
||||||
|
name: `${stack}-cluster`
|
||||||
|
})).arn,
|
||||||
|
networkConfiguration: {
|
||||||
|
subnets: vpc.publicSubnetIds,
|
||||||
|
securityGroups: [fargateSecGroup.id],
|
||||||
|
assignPublicIp: true
|
||||||
|
},
|
||||||
|
desiredCount: containerInstances,
|
||||||
|
taskDefinitionArgs: {
|
||||||
|
family: `${stack}-fg-task-definition`,
|
||||||
|
container: {
|
||||||
|
name: "activepieces",
|
||||||
|
image: imageName,
|
||||||
|
cpu: containerCpu,
|
||||||
|
memory: containerMemory,
|
||||||
|
portMappings: [{
|
||||||
|
targetGroup: alb.defaultTargetGroup,
|
||||||
|
}],
|
||||||
|
environment: environmentVariables
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pulumi.log.info("Finished running Pulumi");
|
||||||
|
|
||||||
|
export const _ = {
|
||||||
|
activePiecesUrl: frontendUrl,
|
||||||
|
activepiecesEnv: environmentVariables
|
||||||
|
};
|
||||||
13
activepieces-fork/deploy/pulumi/package.json
Normal file
13
activepieces-fork/deploy/pulumi/package.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "pulumi",
|
||||||
|
"main": "index.ts",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@pulumi/pulumi": "^3.0.0",
|
||||||
|
"@pulumi/aws": "^6.0.0",
|
||||||
|
"@pulumi/awsx": "^2.20.0",
|
||||||
|
"@pulumi/docker": "^4.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
237
activepieces-fork/deploy/pulumi/taggable.ts
Normal file
237
activepieces-fork/deploy/pulumi/taggable.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* isTaggable returns true if the given resource type is an AWS resource that supports tags.
|
||||||
|
*/
|
||||||
|
export function isTaggable(t: string): boolean {
|
||||||
|
return (taggableResourceTypes.indexOf(t) !== -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// taggableResourceTypes is a list of known AWS type tokens that are taggable.
|
||||||
|
const taggableResourceTypes = [
|
||||||
|
"aws:accessanalyzer/analyzer:Analyzer",
|
||||||
|
"aws:acm/certificate:Certificate",
|
||||||
|
"aws:acmpca/certificateAuthority:CertificateAuthority",
|
||||||
|
"aws:alb/loadBalancer:LoadBalancer",
|
||||||
|
"aws:alb/targetGroup:TargetGroup",
|
||||||
|
"aws:apigateway/apiKey:ApiKey",
|
||||||
|
"aws:apigateway/clientCertificate:ClientCertificate",
|
||||||
|
"aws:apigateway/domainName:DomainName",
|
||||||
|
"aws:apigateway/restApi:RestApi",
|
||||||
|
"aws:apigateway/stage:Stage",
|
||||||
|
"aws:apigateway/usagePlan:UsagePlan",
|
||||||
|
"aws:apigateway/vpcLink:VpcLink",
|
||||||
|
"aws:applicationloadbalancing/loadBalancer:LoadBalancer",
|
||||||
|
"aws:applicationloadbalancing/targetGroup:TargetGroup",
|
||||||
|
"aws:appmesh/mesh:Mesh",
|
||||||
|
"aws:appmesh/route:Route",
|
||||||
|
"aws:appmesh/virtualNode:VirtualNode",
|
||||||
|
"aws:appmesh/virtualRouter:VirtualRouter",
|
||||||
|
"aws:appmesh/virtualService:VirtualService",
|
||||||
|
"aws:appsync/graphQLApi:GraphQLApi",
|
||||||
|
"aws:athena/workgroup:Workgroup",
|
||||||
|
"aws:autoscaling/group:Group",
|
||||||
|
"aws:backup/plan:Plan",
|
||||||
|
"aws:backup/vault:Vault",
|
||||||
|
"aws:cfg/aggregateAuthorization:AggregateAuthorization",
|
||||||
|
"aws:cfg/configurationAggregator:ConfigurationAggregator",
|
||||||
|
"aws:cfg/rule:Rule",
|
||||||
|
"aws:cloudformation/stack:Stack",
|
||||||
|
"aws:cloudformation/stackSet:StackSet",
|
||||||
|
"aws:cloudfront/distribution:Distribution",
|
||||||
|
"aws:cloudhsmv2/cluster:Cluster",
|
||||||
|
"aws:cloudtrail/trail:Trail",
|
||||||
|
"aws:cloudwatch/eventRule:EventRule",
|
||||||
|
"aws:cloudwatch/logGroup:LogGroup",
|
||||||
|
"aws:cloudwatch/metricAlarm:MetricAlarm",
|
||||||
|
"aws:codebuild/project:Project",
|
||||||
|
"aws:codecommit/repository:Repository",
|
||||||
|
"aws:codepipeline/pipeline:Pipeline",
|
||||||
|
"aws:codepipeline/webhook:Webhook",
|
||||||
|
"aws:codestarnotifications/notificationRule:NotificationRule",
|
||||||
|
"aws:cognito/identityPool:IdentityPool",
|
||||||
|
"aws:cognito/userPool:UserPool",
|
||||||
|
"aws:datapipeline/pipeline:Pipeline",
|
||||||
|
"aws:datasync/agent:Agent",
|
||||||
|
"aws:datasync/efsLocation:EfsLocation",
|
||||||
|
"aws:datasync/locationSmb:LocationSmb",
|
||||||
|
"aws:datasync/nfsLocation:NfsLocation",
|
||||||
|
"aws:datasync/s3Location:S3Location",
|
||||||
|
"aws:datasync/task:Task",
|
||||||
|
"aws:dax/cluster:Cluster",
|
||||||
|
"aws:directconnect/connection:Connection",
|
||||||
|
"aws:directconnect/hostedPrivateVirtualInterfaceAccepter:HostedPrivateVirtualInterfaceAccepter",
|
||||||
|
"aws:directconnect/hostedPublicVirtualInterfaceAccepter:HostedPublicVirtualInterfaceAccepter",
|
||||||
|
"aws:directconnect/hostedTransitVirtualInterfaceAcceptor:HostedTransitVirtualInterfaceAcceptor",
|
||||||
|
"aws:directconnect/linkAggregationGroup:LinkAggregationGroup",
|
||||||
|
"aws:directconnect/privateVirtualInterface:PrivateVirtualInterface",
|
||||||
|
"aws:directconnect/publicVirtualInterface:PublicVirtualInterface",
|
||||||
|
"aws:directconnect/transitVirtualInterface:TransitVirtualInterface",
|
||||||
|
"aws:directoryservice/directory:Directory",
|
||||||
|
"aws:dlm/lifecyclePolicy:LifecyclePolicy",
|
||||||
|
"aws:dms/endpoint:Endpoint",
|
||||||
|
"aws:dms/replicationInstance:ReplicationInstance",
|
||||||
|
"aws:dms/replicationSubnetGroup:ReplicationSubnetGroup",
|
||||||
|
"aws:dms/replicationTask:ReplicationTask",
|
||||||
|
"aws:docdb/cluster:Cluster",
|
||||||
|
"aws:docdb/clusterInstance:ClusterInstance",
|
||||||
|
"aws:docdb/clusterParameterGroup:ClusterParameterGroup",
|
||||||
|
"aws:docdb/subnetGroup:SubnetGroup",
|
||||||
|
"aws:dynamodb/table:Table",
|
||||||
|
"aws:ebs/snapshot:Snapshot",
|
||||||
|
"aws:ebs/snapshotCopy:SnapshotCopy",
|
||||||
|
"aws:ebs/volume:Volume",
|
||||||
|
"aws:ec2/ami:Ami",
|
||||||
|
"aws:ec2/amiCopy:AmiCopy",
|
||||||
|
"aws:ec2/amiFromInstance:AmiFromInstance",
|
||||||
|
"aws:ec2/capacityReservation:CapacityReservation",
|
||||||
|
"aws:ec2/customerGateway:CustomerGateway",
|
||||||
|
"aws:ec2/defaultNetworkAcl:DefaultNetworkAcl",
|
||||||
|
"aws:ec2/defaultRouteTable:DefaultRouteTable",
|
||||||
|
"aws:ec2/defaultSecurityGroup:DefaultSecurityGroup",
|
||||||
|
"aws:ec2/defaultSubnet:DefaultSubnet",
|
||||||
|
"aws:ec2/defaultVpc:DefaultVpc",
|
||||||
|
"aws:ec2/defaultVpcDhcpOptions:DefaultVpcDhcpOptions",
|
||||||
|
"aws:ec2/eip:Eip",
|
||||||
|
"aws:ec2/fleet:Fleet",
|
||||||
|
"aws:ec2/instance:Instance",
|
||||||
|
"aws:ec2/internetGateway:InternetGateway",
|
||||||
|
"aws:ec2/keyPair:KeyPair",
|
||||||
|
"aws:ec2/launchTemplate:LaunchTemplate",
|
||||||
|
"aws:ec2/natGateway:NatGateway",
|
||||||
|
"aws:ec2/networkAcl:NetworkAcl",
|
||||||
|
"aws:ec2/networkInterface:NetworkInterface",
|
||||||
|
"aws:ec2/placementGroup:PlacementGroup",
|
||||||
|
"aws:ec2/routeTable:RouteTable",
|
||||||
|
"aws:ec2/securityGroup:SecurityGroup",
|
||||||
|
"aws:ec2/spotInstanceRequest:SpotInstanceRequest",
|
||||||
|
"aws:ec2/subnet:Subnet",
|
||||||
|
"aws:ec2/vpc:Vpc",
|
||||||
|
"aws:ec2/vpcDhcpOptions:VpcDhcpOptions",
|
||||||
|
"aws:ec2/vpcEndpoint:VpcEndpoint",
|
||||||
|
"aws:ec2/vpcEndpointService:VpcEndpointService",
|
||||||
|
"aws:ec2/vpcPeeringConnection:VpcPeeringConnection",
|
||||||
|
"aws:ec2/vpcPeeringConnectionAccepter:VpcPeeringConnectionAccepter",
|
||||||
|
"aws:ec2/vpnConnection:VpnConnection",
|
||||||
|
"aws:ec2/vpnGateway:VpnGateway",
|
||||||
|
"aws:ec2clientvpn/endpoint:Endpoint",
|
||||||
|
"aws:ec2transitgateway/routeTable:RouteTable",
|
||||||
|
"aws:ec2transitgateway/transitGateway:TransitGateway",
|
||||||
|
"aws:ec2transitgateway/vpcAttachment:VpcAttachment",
|
||||||
|
"aws:ec2transitgateway/vpcAttachmentAccepter:VpcAttachmentAccepter",
|
||||||
|
"aws:ecr/repository:Repository",
|
||||||
|
"aws:ecs/capacityProvider:CapacityProvider",
|
||||||
|
"aws:ecs/cluster:Cluster",
|
||||||
|
"aws:ecs/service:Service",
|
||||||
|
"aws:ecs/taskDefinition:TaskDefinition",
|
||||||
|
"aws:efs/fileSystem:FileSystem",
|
||||||
|
"aws:eks/cluster:Cluster",
|
||||||
|
"aws:eks/fargateProfile:FargateProfile",
|
||||||
|
"aws:eks/nodeGroup:NodeGroup",
|
||||||
|
"aws:elasticache/cluster:Cluster",
|
||||||
|
"aws:elasticache/replicationGroup:ReplicationGroup",
|
||||||
|
"aws:elasticbeanstalk/application:Application",
|
||||||
|
"aws:elasticbeanstalk/applicationVersion:ApplicationVersion",
|
||||||
|
"aws:elasticbeanstalk/environment:Environment",
|
||||||
|
"aws:elasticloadbalancing/loadBalancer:LoadBalancer",
|
||||||
|
"aws:elasticloadbalancingv2/loadBalancer:LoadBalancer",
|
||||||
|
"aws:elasticloadbalancingv2/targetGroup:TargetGroup",
|
||||||
|
"aws:elasticsearch/domain:Domain",
|
||||||
|
"aws:elb/loadBalancer:LoadBalancer",
|
||||||
|
"aws:emr/cluster:Cluster",
|
||||||
|
"aws:fsx/lustreFileSystem:LustreFileSystem",
|
||||||
|
"aws:fsx/windowsFileSystem:WindowsFileSystem",
|
||||||
|
"aws:gamelift/alias:Alias",
|
||||||
|
"aws:gamelift/build:Build",
|
||||||
|
"aws:gamelift/fleet:Fleet",
|
||||||
|
"aws:gamelift/gameSessionQueue:GameSessionQueue",
|
||||||
|
"aws:glacier/vault:Vault",
|
||||||
|
"aws:glue/crawler:Crawler",
|
||||||
|
"aws:glue/job:Job",
|
||||||
|
"aws:glue/trigger:Trigger",
|
||||||
|
"aws:iam/role:Role",
|
||||||
|
"aws:iam/user:User",
|
||||||
|
"aws:inspector/resourceGroup:ResourceGroup",
|
||||||
|
"aws:kinesis/analyticsApplication:AnalyticsApplication",
|
||||||
|
"aws:kinesis/firehoseDeliveryStream:FirehoseDeliveryStream",
|
||||||
|
"aws:kinesis/stream:Stream",
|
||||||
|
"aws:kms/externalKey:ExternalKey",
|
||||||
|
"aws:kms/key:Key",
|
||||||
|
"aws:lambda/function:Function",
|
||||||
|
"aws:lb/loadBalancer:LoadBalancer",
|
||||||
|
"aws:lb/targetGroup:TargetGroup",
|
||||||
|
"aws:licensemanager/licenseConfiguration:LicenseConfiguration",
|
||||||
|
"aws:lightsail/instance:Instance",
|
||||||
|
"aws:mediaconvert/queue:Queue",
|
||||||
|
"aws:mediapackage/channel:Channel",
|
||||||
|
"aws:mediastore/container:Container",
|
||||||
|
"aws:mq/broker:Broker",
|
||||||
|
"aws:mq/configuration:Configuration",
|
||||||
|
"aws:msk/cluster:Cluster",
|
||||||
|
"aws:neptune/cluster:Cluster",
|
||||||
|
"aws:neptune/clusterInstance:ClusterInstance",
|
||||||
|
"aws:neptune/clusterParameterGroup:ClusterParameterGroup",
|
||||||
|
"aws:neptune/eventSubscription:EventSubscription",
|
||||||
|
"aws:neptune/parameterGroup:ParameterGroup",
|
||||||
|
"aws:neptune/subnetGroup:SubnetGroup",
|
||||||
|
"aws:opsworks/stack:Stack",
|
||||||
|
"aws:organizations/account:Account",
|
||||||
|
"aws:pinpoint/app:App",
|
||||||
|
"aws:qldb/ledger:Ledger",
|
||||||
|
"aws:ram/resourceShare:ResourceShare",
|
||||||
|
"aws:rds/cluster:Cluster",
|
||||||
|
"aws:rds/clusterEndpoint:ClusterEndpoint",
|
||||||
|
"aws:rds/clusterInstance:ClusterInstance",
|
||||||
|
"aws:rds/clusterParameterGroup:ClusterParameterGroup",
|
||||||
|
"aws:rds/clusterSnapshot:ClusterSnapshot",
|
||||||
|
"aws:rds/eventSubscription:EventSubscription",
|
||||||
|
"aws:rds/instance:Instance",
|
||||||
|
"aws:rds/optionGroup:OptionGroup",
|
||||||
|
"aws:rds/parameterGroup:ParameterGroup",
|
||||||
|
"aws:rds/securityGroup:SecurityGroup",
|
||||||
|
"aws:rds/snapshot:Snapshot",
|
||||||
|
"aws:rds/subnetGroup:SubnetGroup",
|
||||||
|
"aws:redshift/cluster:Cluster",
|
||||||
|
"aws:redshift/eventSubscription:EventSubscription",
|
||||||
|
"aws:redshift/parameterGroup:ParameterGroup",
|
||||||
|
"aws:redshift/snapshotCopyGrant:SnapshotCopyGrant",
|
||||||
|
"aws:redshift/snapshotSchedule:SnapshotSchedule",
|
||||||
|
"aws:redshift/subnetGroup:SubnetGroup",
|
||||||
|
"aws:resourcegroups/group:Group",
|
||||||
|
"aws:route53/healthCheck:HealthCheck",
|
||||||
|
"aws:route53/resolverEndpoint:ResolverEndpoint",
|
||||||
|
"aws:route53/resolverRule:ResolverRule",
|
||||||
|
"aws:route53/zone:Zone",
|
||||||
|
"aws:s3/bucket:Bucket",
|
||||||
|
"aws:s3/bucketObject:BucketObject",
|
||||||
|
"aws:sagemaker/endpoint:Endpoint",
|
||||||
|
"aws:sagemaker/endpointConfiguration:EndpointConfiguration",
|
||||||
|
"aws:sagemaker/model:Model",
|
||||||
|
"aws:sagemaker/notebookInstance:NotebookInstance",
|
||||||
|
"aws:secretsmanager/secret:Secret",
|
||||||
|
"aws:servicecatalog/portfolio:Portfolio",
|
||||||
|
"aws:sfn/activity:Activity",
|
||||||
|
"aws:sfn/stateMachine:StateMachine",
|
||||||
|
"aws:sns/topic:Topic",
|
||||||
|
"aws:sqs/queue:Queue",
|
||||||
|
"aws:ssm/activation:Activation",
|
||||||
|
"aws:ssm/document:Document",
|
||||||
|
"aws:ssm/maintenanceWindow:MaintenanceWindow",
|
||||||
|
"aws:ssm/parameter:Parameter",
|
||||||
|
"aws:ssm/patchBaseline:PatchBaseline",
|
||||||
|
"aws:storagegateway/cachesIscsiVolume:CachesIscsiVolume",
|
||||||
|
"aws:storagegateway/gateway:Gateway",
|
||||||
|
"aws:storagegateway/nfsFileShare:NfsFileShare",
|
||||||
|
"aws:storagegateway/smbFileShare:SmbFileShare",
|
||||||
|
"aws:swf/domain:Domain",
|
||||||
|
"aws:transfer/server:Server",
|
||||||
|
"aws:transfer/user:User",
|
||||||
|
"aws:waf/rateBasedRule:RateBasedRule",
|
||||||
|
"aws:waf/rule:Rule",
|
||||||
|
"aws:waf/ruleGroup:RuleGroup",
|
||||||
|
"aws:waf/webAcl:WebAcl",
|
||||||
|
"aws:wafregional/rateBasedRule:RateBasedRule",
|
||||||
|
"aws:wafregional/rule:Rule",
|
||||||
|
"aws:wafregional/ruleGroup:RuleGroup",
|
||||||
|
"aws:wafregional/webAcl:WebAcl",
|
||||||
|
"aws:workspaces/directory:Directory",
|
||||||
|
"aws:workspaces/ipGroup:IpGroup",
|
||||||
|
];
|
||||||
20
activepieces-fork/deploy/pulumi/tsconfig.json
Normal file
20
activepieces-fork/deploy/pulumi/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"outDir": "bin",
|
||||||
|
"target": "es2016",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"pretty": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"index.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
activepieces-fork/depot.json
Normal file
1
activepieces-fork/depot.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"id":"du7O4b0e8P"}
|
||||||
23
activepieces-fork/docker-compose.dev.yml
Normal file
23
activepieces-fork/docker-compose.dev.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:14.4
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: activepieces
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: A79Vm5D4p2VQHOp2gd5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7.0.7
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
28
activepieces-fork/docker-compose.test.yml
Normal file
28
activepieces-fork/docker-compose.test.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.dev.yml
|
||||||
|
service: app
|
||||||
|
user: "${UID}:${GID}"
|
||||||
|
command: /bin/sh -c "npm_config_cache=/usr/src/app/.npm-cache npx nx run-tests backend"
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.dev.yml
|
||||||
|
service: postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.dev.yml
|
||||||
|
service: redis
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data_dev:
|
||||||
|
redis_data_dev:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
activepieces_dev:
|
||||||
43
activepieces-fork/docker-compose.yml
Normal file
43
activepieces-fork/docker-compose.yml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
services:
|
||||||
|
activepieces:
|
||||||
|
image: ghcr.io/activepieces/activepieces:0.74.3
|
||||||
|
container_name: activepieces
|
||||||
|
restart: unless-stopped
|
||||||
|
## Enable the following line if you already use AP_EXECUTION_MODE with SANDBOX_PROCESS or old activepieces, checking the breaking change documentation for more info.
|
||||||
|
## privileged: true
|
||||||
|
ports:
|
||||||
|
- '8080:80'
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./cache:/usr/src/app/cache
|
||||||
|
networks:
|
||||||
|
- activepieces
|
||||||
|
postgres:
|
||||||
|
image: 'postgres:14.4'
|
||||||
|
container_name: postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
- 'POSTGRES_DB=${AP_POSTGRES_DATABASE}'
|
||||||
|
- 'POSTGRES_PASSWORD=${AP_POSTGRES_PASSWORD}'
|
||||||
|
- 'POSTGRES_USER=${AP_POSTGRES_USERNAME}'
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- activepieces
|
||||||
|
redis:
|
||||||
|
image: 'redis:7.0.7'
|
||||||
|
container_name: redis
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- 'redis_data:/data'
|
||||||
|
networks:
|
||||||
|
- activepieces
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
networks:
|
||||||
|
activepieces:
|
||||||
26
activepieces-fork/docker-entrypoint.sh
Normal file
26
activepieces-fork/docker-entrypoint.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Set default values if not provided
|
||||||
|
export AP_APP_TITLE="${AP_APP_TITLE:-Activepieces}"
|
||||||
|
export AP_FAVICON_URL="${AP_FAVICON_URL:-https://cdn.activepieces.com/brand/favicon.ico}"
|
||||||
|
|
||||||
|
# Debug: Print environment variables
|
||||||
|
echo "AP_APP_TITLE: $AP_APP_TITLE"
|
||||||
|
echo "AP_FAVICON_URL: $AP_FAVICON_URL"
|
||||||
|
|
||||||
|
# Process environment variables in index.html BEFORE starting services
|
||||||
|
envsubst '${AP_APP_TITLE} ${AP_FAVICON_URL}' < /usr/share/nginx/html/index.html > /usr/share/nginx/html/index.html.tmp && \
|
||||||
|
mv /usr/share/nginx/html/index.html.tmp /usr/share/nginx/html/index.html
|
||||||
|
|
||||||
|
|
||||||
|
# Start Nginx server
|
||||||
|
nginx -g "daemon off;" &
|
||||||
|
|
||||||
|
# Start backend server
|
||||||
|
if [ "$AP_CONTAINER_TYPE" = "APP" ] && [ "$AP_PM2_ENABLED" = "true" ]; then
|
||||||
|
echo "Starting backend server with PM2 (APP mode)"
|
||||||
|
pm2-runtime start dist/packages/server/api/main.cjs --name "activepieces-app" --node-args="--enable-source-maps" -i 0
|
||||||
|
else
|
||||||
|
echo "Starting backend server with Node.js (WORKER mode or default)"
|
||||||
|
node --enable-source-maps dist/packages/server/api/main.cjs
|
||||||
|
fi
|
||||||
21
activepieces-fork/docs/LICENSE
Normal file
21
activepieces-fork/docs/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Mintlify
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
43
activepieces-fork/docs/README.md
Normal file
43
activepieces-fork/docs/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Mintlify Starter Kit
|
||||||
|
|
||||||
|
Use the starter kit to get your docs deployed and ready to customize.
|
||||||
|
|
||||||
|
Click the green **Use this template** button at the top of this repo to copy the Mintlify starter kit. The starter kit contains examples with
|
||||||
|
|
||||||
|
- Guide pages
|
||||||
|
- Navigation
|
||||||
|
- Customizations
|
||||||
|
- API reference pages
|
||||||
|
- Use of popular components
|
||||||
|
|
||||||
|
**[Follow the full quickstart guide](https://starter.mintlify.com/quickstart)**
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Install the [Mintlify CLI](https://www.npmjs.com/package/mint) to preview your documentation changes locally. To install, use the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm i -g mint
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the following command at the root of your documentation, where your `docs.json` is located:
|
||||||
|
|
||||||
|
```
|
||||||
|
mint dev
|
||||||
|
```
|
||||||
|
|
||||||
|
View your local preview at `http://localhost:3000`.
|
||||||
|
|
||||||
|
## Publishing changes
|
||||||
|
|
||||||
|
Install our GitHub app from your [dashboard](https://dashboard.mintlify.com/settings/organization/github-app) to propagate changes from your repo to your deployment. Changes are deployed to production automatically after pushing to the default branch.
|
||||||
|
|
||||||
|
## Need help?
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
- If your dev environment isn't running: Run `mint update` to ensure you have the most recent version of the CLI.
|
||||||
|
- If a page loads as a 404: Make sure you are running in a folder with a valid `docs.json`.
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
- [Mintlify documentation](https://mintlify.com/docs)
|
||||||
3
activepieces-fork/docs/_snippets/enterprise-feature.mdx
Normal file
3
activepieces-fork/docs/_snippets/enterprise-feature.mdx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Tip>
|
||||||
|
This feature is available in our paid editions. Contact us [here](https://www.activepieces.com/sales), and we'll be delighted to assist you!
|
||||||
|
</Tip>
|
||||||
6
activepieces-fork/docs/_snippets/execution-mode.mdx
Normal file
6
activepieces-fork/docs/_snippets/execution-mode.mdx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
| Name | Supports NPM in Code Piece | Requires Docker to be Privileged | Performance | Secure for Multi Tenant | Reusable Workers | Environment Variable |
|
||||||
|
|-------------------------------|----------------------------|----------------------------------|-----------------------|-------------------------|------------------|-------------------------------------------|
|
||||||
|
| V8/Code Sandboxing | ❌ | No | Fast & Lightweight | ✅ | ✅ | Set `AP_EXECUTION_MODE` to `SANDBOX_CODE_ONLY` |
|
||||||
|
| No Sandboxing | ✅ | No | Fast & Lightweight | ❌ | ✅ | Set `AP_EXECUTION_MODE` to `UNSANDBOXED` |
|
||||||
|
| Kernel Namespaces Sandboxing | ✅ | Yes | Slow & CPU Intensive | ✅ | ❌ | Set `AP_EXECUTION_MODE` to `SANDBOX_PROCESS` |
|
||||||
|
| Combined Sandboxing | ❌ | Yes | Medium & CPU Intensive | ✅ | ✅ | Set `AP_EXECUTION_MODE` to `SANDBOX_CODE_AND_PROCESS` |
|
||||||
3
activepieces-fork/docs/_snippets/replace-oauth2-apps.mdx
Normal file
3
activepieces-fork/docs/_snippets/replace-oauth2-apps.mdx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Tip>
|
||||||
|
If you would like your users to use your own OAuth2 apps, we recommend you check [this](/admin-guide/guides/manage-oauth2).
|
||||||
|
</Tip>
|
||||||
6
activepieces-fork/docs/about/changelog.mdx
Executable file
6
activepieces-fork/docs/about/changelog.mdx
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
title: "Changelog"
|
||||||
|
description: "A log of all notable changes to Activepieces"
|
||||||
|
icon: "code-commit"
|
||||||
|
url: "https://github.com/activepieces/activepieces/releases"
|
||||||
|
---
|
||||||
29
activepieces-fork/docs/about/i18n.mdx
Normal file
29
activepieces-fork/docs/about/i18n.mdx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
title: "i18n Translations"
|
||||||
|
description: ""
|
||||||
|
icon: "language"
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
This guide helps you understand how to change or add new translations.
|
||||||
|
|
||||||
|
Activepieces uses Crowdin because it helps translators who don't know how to code. It also makes the approval process easier. Activepieces automatically sync new text from the code and translations back into the code.
|
||||||
|
|
||||||
|
## Contribute to existing translations
|
||||||
|
|
||||||
|
1. Create Crowdin account
|
||||||
|
2. Join the project https://crowdin.com/project/activepieces
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Click on the language you want to translate
|
||||||
|
|
||||||
|
4. Click on "Translate All"
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Select Strings you want to translate and click on "Save" button
|
||||||
|
|
||||||
|
|
||||||
|
## Adding a new language
|
||||||
|
- Please contact us (support@activepieces.com) if you want to add a new language. We will add it to the project and you can start translating.
|
||||||
22
activepieces-fork/docs/about/license.mdx
Executable file
22
activepieces-fork/docs/about/license.mdx
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: "License"
|
||||||
|
description: ""
|
||||||
|
icon: 'file-contract'
|
||||||
|
---
|
||||||
|
|
||||||
|
Activepieces' **core** is released as open source under the [MIT license](https://github.com/activepieces/activepieces/blob/main/LICENSE) and enterprise / cloud editions features are released under [Commercial License](https://github.com/activepieces/activepieces/blob/main/packages/ee/LICENSE)
|
||||||
|
|
||||||
|
The MIT license is a permissive license that grants users the freedom to use, modify, or distribute the software without any significant restrictions. The only requirement is that you include the license notice along with the software when distributing it.
|
||||||
|
|
||||||
|
Using the enterprise features (under the packages/ee and packages/server/api/src/app/ee folder) with a self-hosted instance requires an Activepieces license. If you are looking for these features, contact us at [sales@activepieces.com](mailto:sales@activepieces.com).
|
||||||
|
|
||||||
|
**Benefits of Dual Licensing Repo**
|
||||||
|
|
||||||
|
- **Transparency** - Everyone can see what we are doing and contribute to the project.
|
||||||
|
- **Clarity** - Everyone can see what the difference is between the open source and commercial versions of our software.
|
||||||
|
- **Audit** - Everyone can audit our code and see what we are doing.
|
||||||
|
- **Faster Development** - We can develop faster and more efficiently.
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
If you are still confused or have feedback, please open an issue on GitHub or send a message in the #contribution channel on Discord.
|
||||||
|
</Tip>
|
||||||
26
activepieces-fork/docs/admin-guide/guides/manage-oauth2.mdx
Normal file
26
activepieces-fork/docs/admin-guide/guides/manage-oauth2.mdx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: "Override OAuth2 Apps"
|
||||||
|
description: "Use your own OAuth2 credentials instead of the default Activepieces apps"
|
||||||
|
icon: "lock"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Snippet file="enterprise-feature.mdx" />
|
||||||
|
|
||||||
|
## Default Behavior
|
||||||
|
|
||||||
|
When users connect to services like Google Sheets or Slack, they see "Activepieces" as the app requesting access. This works out of the box with no setup required.
|
||||||
|
|
||||||
|
## Why Replace OAuth2 Apps?
|
||||||
|
|
||||||
|
- **Branding**: Show your company name instead of "Activepieces" in authorization screens
|
||||||
|
- **Higher Limits**: Some services have stricter rate limits for shared OAuth apps
|
||||||
|
- **Compliance**: Your organization may require using company-owned credentials
|
||||||
|
|
||||||
|
## How to Configure
|
||||||
|
|
||||||
|
1. Go to **Platform Admin → Setup → Pieces**
|
||||||
|
2. Find the piece you want to configure (e.g., Google Sheets)
|
||||||
|
3. Click the lock icon to open the OAuth2 settings
|
||||||
|
4. Enter your own Client ID and Client Secret
|
||||||
|
|
||||||
|

|
||||||
88
activepieces-fork/docs/admin-guide/guides/manage-pieces.mdx
Normal file
88
activepieces-fork/docs/admin-guide/guides/manage-pieces.mdx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
title: "How to Manage Pieces"
|
||||||
|
description: "Control which integrations are available to your users"
|
||||||
|
icon: "puzzle-piece"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Snippet file="enterprise-feature.mdx" />
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**Pieces** are the building blocks of Activepieces — they are integrations and connectors (like Google Sheets, Slack, OpenAI, etc.) that users can use in their automation flows.
|
||||||
|
|
||||||
|
As a platform administrator, you have full control over which pieces are available to your users. This allows you to:
|
||||||
|
|
||||||
|
- **Enforce security policies** by restricting access to certain integrations
|
||||||
|
- **Simplify the user experience** by showing only relevant pieces for your use case
|
||||||
|
- **Deploy custom/private pieces** that are specific to your organization
|
||||||
|
|
||||||
|
There are **two levels** of piece management:
|
||||||
|
|
||||||
|
| Level | Who Can Manage | Scope |
|
||||||
|
|-------|----------------|-------|
|
||||||
|
| **Platform Level** | Platform Admin | Install and remove across the entire platform |
|
||||||
|
| **Project Level** | Project Admin | Show/hide specific pieces for specfic project |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Platform-Level Management
|
||||||
|
|
||||||
|
Platform administrators can manage pieces for the entire Activepieces instance from **Platform Admin → Setup → Pieces**.
|
||||||
|
|
||||||
|
## Project-Level Management
|
||||||
|
|
||||||
|
Project administrators can further restrict which pieces are available within their specific project. This is useful when different teams or projects need access to different integrations.
|
||||||
|
|
||||||
|
### Show/Hide Pieces in a Project
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Open Project Settings">
|
||||||
|
Navigate to your project and go to **Settings → Pieces**.
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure Visibility">
|
||||||
|
You'll see a list of all pieces installed on the platform. Toggle the visibility for each piece:
|
||||||
|
- **Enabled**: Users in this project can use the piece
|
||||||
|
- **Disabled**: The piece is hidden from users in this project
|
||||||
|
</Step>
|
||||||
|
<Step title="Save Changes">
|
||||||
|
Changes take effect immediately — users will only see the enabled pieces when building their flows.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Project-level settings can only **hide** pieces that are installed at the platform level. You cannot add pieces at the project level that aren't already installed on the platform.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
|
||||||
|
### Install Private Pieces
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
For detailed instructions on building custom pieces, check the [Building Pieces](/build-pieces/building-pieces/overview) documentation.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
|
||||||
|
If you've built a custom piece for your organization, you can upload it directly as a tarball (`.tgz`) file.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Build Your Piece">
|
||||||
|
Build your piece using the Activepieces CLI:
|
||||||
|
```bash
|
||||||
|
npm run pieces -- build --name=your-piece-name
|
||||||
|
```
|
||||||
|
This generates a tarball in `dist/packages/pieces/your-piece-name`.
|
||||||
|
</Step>
|
||||||
|
<Step title="Navigate to Pieces Settings">
|
||||||
|
Go to **Platform Admin → Setup → Pieces** and click **Install Piece**.
|
||||||
|
</Step>
|
||||||
|
<Step title="Select File Upload">
|
||||||
|
Choose **Upload File** as the installation source.
|
||||||
|
</Step>
|
||||||
|
<Step title="Upload the Tarball">
|
||||||
|
Select the `.tgz` file from your build output and upload it.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|

|
||||||
53
activepieces-fork/docs/admin-guide/guides/permissions.mdx
Normal file
53
activepieces-fork/docs/admin-guide/guides/permissions.mdx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
title: "Manage User Roles"
|
||||||
|
description: "Documentation on project permissions in Activepieces"
|
||||||
|
icon: 'user'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Snippet file="enterprise-feature.mdx" />
|
||||||
|
|
||||||
|
Activepieces utilizes Role-Based Access Control (RBAC) for managing permissions within projects. Each project consists of multiple flows and users, with each user assigned specific roles that define their actions within the project.
|
||||||
|
|
||||||
|
## Default Roles
|
||||||
|
|
||||||
|
Activepieces comes with four standard roles out of the box. The table below shows the permissions for each role:
|
||||||
|
|
||||||
|
| Permission | Admin | Editor | Operator | Viewer |
|
||||||
|
|------------|:-----:|:------:|:--------:|:------:|
|
||||||
|
| **Flows** |||||
|
||||||
|
| View Flows | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Edit Flows | ✓ | ✓ | | |
|
||||||
|
| Publish / Toggle Flows | ✓ | ✓ | ✓ | |
|
||||||
|
| **Runs** |||||
|
||||||
|
| View Runs | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Retry Runs | ✓ | ✓ | ✓ | |
|
||||||
|
| **Connections** |||||
|
||||||
|
| View Connections | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Edit Connections | ✓ | ✓ | ✓ | |
|
||||||
|
| **Team** |||||
|
||||||
|
| View Project Members | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Add/Remove Project Members | ✓ | | | |
|
||||||
|
| **Git Sync** | | | | |
|
||||||
|
| Configure Git Repo | ✓ | | | |
|
||||||
|
| Pull Flows from Git | ✓ | | | |
|
||||||
|
| Push Flows to Git | ✓ | | | |
|
||||||
|
|
||||||
|
## Custom Roles
|
||||||
|
|
||||||
|
If the default roles don't fit your needs, you can create custom roles with specific permissions.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to Project Roles">
|
||||||
|
Go to **Platform Admin** → **Security** → **Project Roles**
|
||||||
|
</Step>
|
||||||
|
<Step title="Create a New Role">
|
||||||
|
Click **Create Role** and give it a name
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure Permissions">
|
||||||
|
Select the specific permissions you want to grant to this role
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Custom roles are useful when you need fine-grained control, such as allowing users to view and retry runs without being able to edit flows.
|
||||||
|
</Tip>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: "Setup AI Providers"
|
||||||
|
description: ""
|
||||||
|
icon: "sparkles"
|
||||||
|
---
|
||||||
|
|
||||||
|
AI providers are configured by the platform admin to centrally manage credentials and access, making [AI pieces](https://www.activepieces.com/pieces/ai) and their features available to everyone in all projects.
|
||||||
|
|
||||||
|
## Supported Providers
|
||||||
|
|
||||||
|
- **OpenAI**
|
||||||
|
- **Anthropic**
|
||||||
|
- **Gemini**
|
||||||
|
- **Vercel AI Gateway**
|
||||||
|
- **Cloudflare AI Gateway**
|
||||||
|
|
||||||
|
## How to Setup
|
||||||
|
|
||||||
|
Go to **Admin Console** → **AI** page. Add your provider's base URL and API key. These settings apply to all projects.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Cost Control & Logging
|
||||||
|
|
||||||
|
Use an AI gateway like **Vercel AI Gateway** or **Cloudflare AI Gateway** to:
|
||||||
|
|
||||||
|
- Set rate limits and budgets
|
||||||
|
- Log and monitor all AI requests
|
||||||
|
- Track usage across projects
|
||||||
|
|
||||||
|
Just set the gateway URL as your provider's base URL in the Admin Console.
|
||||||
223
activepieces-fork/docs/admin-guide/guides/sso.mdx
Normal file
223
activepieces-fork/docs/admin-guide/guides/sso.mdx
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
---
|
||||||
|
title: "How to Setup SSO"
|
||||||
|
description: "Configure Single Sign-On (SSO) to enable secure, centralized authentication for your Activepieces platform"
|
||||||
|
icon: 'key'
|
||||||
|
---
|
||||||
|
|
||||||
|
<Snippet file="enterprise-feature.mdx" />
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Single Sign-On (SSO) allows your team to authenticate using your organization's existing identity provider, eliminating the need for separate Activepieces credentials. This improves security, simplifies user management, and provides a seamless login experience.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before configuring SSO, ensure you have:
|
||||||
|
|
||||||
|
- **Admin access** to your Activepieces platform
|
||||||
|
- **Admin access** to your identity provider (Google, GitHub, Okta, or JumpCloud)
|
||||||
|
- The **redirect URL** from your Activepieces SSO configuration screen
|
||||||
|
|
||||||
|
## Accessing SSO Configuration
|
||||||
|
|
||||||
|
Navigate to **Platform Settings** → **SSO** in your Activepieces admin dashboard to access the SSO configuration screen.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Enforcing SSO
|
||||||
|
|
||||||
|
You can enforce SSO by specifying your organization's email domain. When SSO enforcement is enabled:
|
||||||
|
|
||||||
|
- Users with matching email domains must authenticate through the SSO provider
|
||||||
|
- Email/password login can be disabled for enhanced security
|
||||||
|
- All authentication is routed through your designated identity provider
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
We recommend testing SSO with a small group of users before enforcing it organization-wide.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
## Supported SSO Providers
|
||||||
|
|
||||||
|
Activepieces supports multiple SSO providers to integrate with your existing identity management system.
|
||||||
|
|
||||||
|
### Google
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Access Google Cloud Console">
|
||||||
|
Go to the [Google Cloud Console](https://console.cloud.google.com/) and select your project (or create a new one).
|
||||||
|
</Step>
|
||||||
|
<Step title="Create OAuth2 Credentials">
|
||||||
|
Navigate to **APIs & Services** → **Credentials** → **Create Credentials** → **OAuth client ID**.
|
||||||
|
|
||||||
|
Select **Web application** as the application type.
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure Redirect URI">
|
||||||
|
Copy the **Redirect URL** from the Activepieces SSO configuration screen and add it to the **Authorized redirect URIs** in Google Cloud Console.
|
||||||
|
</Step>
|
||||||
|
<Step title="Copy Credentials to Activepieces">
|
||||||
|
Copy the **Client ID** and **Client Secret** from Google and paste them into the corresponding fields in Activepieces.
|
||||||
|
</Step>
|
||||||
|
<Step title="Save Configuration">
|
||||||
|
Click **Finish** to complete the setup.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
### GitHub
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Access GitHub Developer Settings">
|
||||||
|
Go to [GitHub Developer Settings](https://github.com/settings/developers) → **OAuth Apps** → **New OAuth App**.
|
||||||
|
</Step>
|
||||||
|
<Step title="Register New Application">
|
||||||
|
Fill in the application details:
|
||||||
|
- **Application name**: Choose a recognizable name (e.g., "Activepieces SSO")
|
||||||
|
- **Homepage URL**: Enter your Activepieces instance URL
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure Authorization Callback">
|
||||||
|
Copy the **Redirect URL** from the Activepieces SSO configuration screen and paste it into the **Authorization callback URL** field.
|
||||||
|
</Step>
|
||||||
|
<Step title="Complete Registration">
|
||||||
|
Click **Register application** to create the OAuth App.
|
||||||
|
</Step>
|
||||||
|
<Step title="Generate Client Secret">
|
||||||
|
After registration, click **Generate a new client secret** and copy it immediately (it won't be shown again).
|
||||||
|
</Step>
|
||||||
|
<Step title="Copy Credentials to Activepieces">
|
||||||
|
Copy the **Client ID** and **Client Secret** and paste them into the corresponding fields in Activepieces.
|
||||||
|
</Step>
|
||||||
|
<Step title="Save Configuration">
|
||||||
|
Click **Finish** to complete the setup.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
### SAML with Okta
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create New Application in Okta">
|
||||||
|
Go to the [Okta Admin Portal](https://login.okta.com/) → **Applications** → **Create App Integration**.
|
||||||
|
</Step>
|
||||||
|
<Step title="Select SAML 2.0">
|
||||||
|
Choose **SAML 2.0** as the sign-on method and click **Next**.
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure General Settings">
|
||||||
|
Enter an **App name** (e.g., "Activepieces") and optionally upload a logo. Click **Next**.
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure SAML Settings">
|
||||||
|
- **Single sign-on URL**: Copy the SSO URL from the Activepieces configuration screen
|
||||||
|
- **Audience URI (SP Entity ID)**: Enter `Activepieces`
|
||||||
|
- **Name ID format**: Select `EmailAddress`
|
||||||
|
</Step>
|
||||||
|
<Step title="Add Attribute Statements">
|
||||||
|
Add the following attribute mappings:
|
||||||
|
|
||||||
|
| Name | Value |
|
||||||
|
|------|-------|
|
||||||
|
| `firstName` | `user.firstName` |
|
||||||
|
| `lastName` | `user.lastName` |
|
||||||
|
| `email` | `user.email` |
|
||||||
|
</Step>
|
||||||
|
<Step title="Complete Setup in Okta">
|
||||||
|
Click **Next**, select the appropriate feedback option, and click **Finish**.
|
||||||
|
</Step>
|
||||||
|
<Step title="Export IdP Metadata">
|
||||||
|
Go to the **Sign On** tab → **View SAML setup instructions** or **View IdP metadata**. Copy the Identity Provider metadata XML.
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure Activepieces">
|
||||||
|
- Paste the **IdP Metadata** XML into the corresponding field
|
||||||
|
- Copy the **X.509 Certificate** from Okta and paste it into the **Signing Key** field
|
||||||
|
</Step>
|
||||||
|
<Step title="Save Configuration">
|
||||||
|
Click **Save** to complete the setup.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
### SAML with JumpCloud
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create New Application in JumpCloud">
|
||||||
|
Go to the [JumpCloud Admin Portal](https://console.jumpcloud.com/) → **SSO Applications** → **Add New Application** → **Custom SAML App**.
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure ACS URL">
|
||||||
|
Copy the **ACS URL** from the Activepieces configuration screen and paste it into the **ACS URLs** field in JumpCloud.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Configure SP Entity ID">
|
||||||
|
Set the **SP Entity ID** (Audience URI) to `Activepieces`.
|
||||||
|
</Step>
|
||||||
|
<Step title="Add User Attributes">
|
||||||
|
Configure the following attribute mappings:
|
||||||
|
|
||||||
|
| Service Provider Attribute | JumpCloud Attribute |
|
||||||
|
|---------------------------|---------------------|
|
||||||
|
| `firstName` | `firstname` |
|
||||||
|
| `lastName` | `lastname` |
|
||||||
|
| `email` | `email` |
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Enable HTTP-Redirect Binding">
|
||||||
|
JumpCloud does not include the `HTTP-Redirect` binding by default. You **must** enable this option.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Without HTTP-Redirect binding, the SSO integration will not work correctly.
|
||||||
|
</Warning>
|
||||||
|
</Step>
|
||||||
|
<Step title="Export Metadata">
|
||||||
|
Click **Save**, then refresh the page and click **Export Metadata**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Verify that the exported XML contains `Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"` to ensure the binding was properly enabled.
|
||||||
|
</Tip>
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure IdP Metadata in Activepieces">
|
||||||
|
Paste the exported metadata XML into the **IdP Metadata** field in Activepieces.
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure Signing Certificate">
|
||||||
|
Locate the `<ds:X509Certificate>` element in the IdP metadata and extract its value. Format it as a PEM certificate:
|
||||||
|
|
||||||
|
```
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
[PASTE THE CERTIFICATE VALUE HERE]
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
```
|
||||||
|
|
||||||
|
Paste this into the **Signing Key** field.
|
||||||
|
</Step>
|
||||||
|
<Step title="Assign Users to Application">
|
||||||
|
In JumpCloud, assign the application to the appropriate users or user groups.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Save Configuration">
|
||||||
|
Click **Finish** to complete the setup.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Users cannot log in after SSO configuration">
|
||||||
|
- Verify the redirect URL is correctly configured in your identity provider
|
||||||
|
- Ensure users are assigned to the application in your identity provider
|
||||||
|
- Check that email domains match the SSO enforcement settings
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="SAML authentication fails">
|
||||||
|
- Confirm the IdP metadata is complete and correctly formatted
|
||||||
|
- Verify the signing certificate is properly formatted with BEGIN/END markers
|
||||||
|
- Ensure all required attributes (firstName, lastName, email) are mapped
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="HTTP-Redirect binding error (JumpCloud)">
|
||||||
|
- Enable the HTTP-Redirect binding option in JumpCloud
|
||||||
|
- Re-export the metadata after enabling the binding
|
||||||
|
- Verify the binding appears in the exported XML
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
If you encounter issues during SSO setup, please contact our enterprise support or [sales team](https://www.activepieces.com/sales).
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: "How to Structure Projects"
|
||||||
|
description: ""
|
||||||
|
icon: "building"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Snippet file="enterprise-feature.mdx" />
|
||||||
|
|
||||||
|
Projects in Activepieces are the main units for organizing your automations and resources within your organization. Every project contains its own flows, connections, and tables. Access to these resources is shared among everyone who has access to that project.
|
||||||
|
|
||||||
|
There are two types of projects:
|
||||||
|
- **Personal Projects**: Each user invited to your organization automatically receives a personal project. This is a private space where only that user can create and manage flows, connections, and tables.
|
||||||
|
- **Team Projects**: Team projects are shared spaces that can be created and managed from this page. Multiple users can be invited to a team project, allowing them to collaborate, share access to flows, connections, and tables, and work together.
|
||||||
|
|
||||||
|
When organizing your work, create team projects for group collaboration and utilize personal projects for individual or private tasks.
|
||||||
23
activepieces-fork/docs/admin-guide/overview.mdx
Normal file
23
activepieces-fork/docs/admin-guide/overview.mdx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
title: "Overview"
|
||||||
|
icon: "hand-wave"
|
||||||
|
description: "Manage and customize your Activepieces instance"
|
||||||
|
---
|
||||||
|
|
||||||
|
The **Platform Admin** is the centralized admin panel for managing your Activepieces instance. It's designed for teams and organizations that want full control over users, integrations, security, and internal automation.
|
||||||
|
|
||||||
|
## What Can You Do?
|
||||||
|
|
||||||
|
With Platform Admin, you can:
|
||||||
|
|
||||||
|
- **Custom Branding:** Tailor the appearance of Activepieces to match your organization's identity, including colors, logos, and fonts.
|
||||||
|
|
||||||
|
- **Project Management:** Create, edit, and organize projects for internal teams and users.
|
||||||
|
|
||||||
|
- **Piece Management:** Control which integration pieces are available, including managing custom or internal pieces for your team's workflows.
|
||||||
|
|
||||||
|
- **User Management:** Add and remove users, send invitations, and assign roles and permissions.
|
||||||
|
|
||||||
|
- **AI Provider Management:** Configure and manage AI providers (like OpenAI, Anthropic, etc.) available for use in your flows.
|
||||||
|
|
||||||
|
- **SSO & Security:** Configure Single Sign-On (SSO) providers and manage security settings to ensure your instance is secure.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'Connection Deleted'
|
||||||
|
openapi-schema: connection.deleted
|
||||||
|
icon: link
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'Connection Upserted'
|
||||||
|
openapi-schema: connection.upserted
|
||||||
|
icon: link
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'Flow Created'
|
||||||
|
openapi-schema: flow.created
|
||||||
|
icon: bolt
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'Flow Deleted'
|
||||||
|
openapi-schema: flow.deleted
|
||||||
|
icon: bolt
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'Flow Run Finished'
|
||||||
|
openapi-schema: flow.run.finished
|
||||||
|
icon: play
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'Flow Run Started'
|
||||||
|
openapi-schema: flow.run.started
|
||||||
|
icon: play
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'Flow Updated'
|
||||||
|
openapi-schema: flow.updated
|
||||||
|
icon: bolt
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'Folder Created'
|
||||||
|
openapi-schema: folder.created
|
||||||
|
icon: folder
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'Folder Deleted'
|
||||||
|
openapi-schema: folder.deleted
|
||||||
|
icon: folder
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'Folder Updated'
|
||||||
|
openapi-schema: folder.updated
|
||||||
|
icon: folder
|
||||||
|
---
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: "Overview"
|
||||||
|
description: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
<Snippet file="enterprise-feature.mdx" />
|
||||||
|
|
||||||
|
This table in admin console contains all application events. We are constantly adding new events, so there is no better place to see the events defined in the code than [here](https://github.com/activepieces/activepieces/blob/main/packages/ee/shared/src/lib/audit-events/index.ts).
|
||||||
|
|
||||||
|

|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'Signing Key Created'
|
||||||
|
openapi-schema: signing.key.created
|
||||||
|
icon: key
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'User Email Verified'
|
||||||
|
openapi-schema: user.email.verified
|
||||||
|
icon: lock
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'User Password Reset'
|
||||||
|
openapi-schema: user.password.reset
|
||||||
|
icon: lock
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: 'User Signed In'
|
||||||
|
openapi-schema: user.signed.in
|
||||||
|
icon: lock
|
||||||
|
---
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user