Compare commits
3 Commits
1531f41226
...
feature/ph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56c30e4904 | ||
|
|
51cd021f85 | ||
|
|
fc6dd5623a |
@@ -14,8 +14,10 @@ from .views import (
|
||||
SiteUserAccessViewSet, PlanViewSet, SiteViewSet, SectorViewSet,
|
||||
IndustryViewSet, SeedKeywordViewSet
|
||||
)
|
||||
from .serializers import RegisterSerializer, LoginSerializer, ChangePasswordSerializer, UserSerializer
|
||||
from .serializers import RegisterSerializer, LoginSerializer, ChangePasswordSerializer, UserSerializer, RefreshTokenSerializer
|
||||
from .models import User
|
||||
from .utils import generate_access_token, get_token_expiry, decode_token
|
||||
import jwt
|
||||
|
||||
router = DefaultRouter()
|
||||
# Main structure: Groups, Users, Accounts, Subscriptions, Site User Access
|
||||
@@ -78,7 +80,7 @@ class LoginView(APIView):
|
||||
password = serializer.validated_data['password']
|
||||
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
user = User.objects.select_related('account', 'account__plan').get(email=email)
|
||||
except User.DoesNotExist:
|
||||
return error_response(
|
||||
error='Invalid credentials',
|
||||
@@ -107,9 +109,17 @@ class LoginView(APIView):
|
||||
user_data = user_serializer.data
|
||||
except Exception as e:
|
||||
# Fallback if serializer fails (e.g., missing account_id column)
|
||||
# Log the error for debugging but don't fail the login
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"UserSerializer failed for user {user.id}: {e}", exc_info=True)
|
||||
|
||||
# Ensure username is properly set (use email prefix if username is empty/default)
|
||||
username = user.username if user.username and user.username != 'user' else user.email.split('@')[0]
|
||||
|
||||
user_data = {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'username': username,
|
||||
'email': user.email,
|
||||
'role': user.role,
|
||||
'account': None,
|
||||
@@ -119,12 +129,10 @@ class LoginView(APIView):
|
||||
return success_response(
|
||||
data={
|
||||
'user': user_data,
|
||||
'tokens': {
|
||||
'access': access_token,
|
||||
'refresh': refresh_token,
|
||||
'access_expires_at': access_expires_at.isoformat(),
|
||||
'refresh_expires_at': refresh_expires_at.isoformat(),
|
||||
}
|
||||
'access': access_token,
|
||||
'refresh': refresh_token,
|
||||
'access_expires_at': access_expires_at.isoformat(),
|
||||
'refresh_expires_at': refresh_expires_at.isoformat(),
|
||||
},
|
||||
message='Login successful',
|
||||
request=request
|
||||
@@ -180,6 +188,84 @@ class ChangePasswordView(APIView):
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
tags=['Authentication'],
|
||||
summary='Refresh Token',
|
||||
description='Refresh access token using refresh token'
|
||||
)
|
||||
class RefreshTokenView(APIView):
|
||||
"""Refresh access token endpoint."""
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
serializer = RefreshTokenSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return error_response(
|
||||
error='Validation failed',
|
||||
errors=serializer.errors,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
refresh_token = serializer.validated_data['refresh']
|
||||
|
||||
try:
|
||||
# Decode and validate refresh token
|
||||
payload = decode_token(refresh_token)
|
||||
|
||||
# Verify it's a refresh token
|
||||
if payload.get('type') != 'refresh':
|
||||
return error_response(
|
||||
error='Invalid token type',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get user
|
||||
user_id = payload.get('user_id')
|
||||
account_id = payload.get('account_id')
|
||||
|
||||
try:
|
||||
user = User.objects.select_related('account', 'account__plan').get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return error_response(
|
||||
error='User not found',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get account
|
||||
account = None
|
||||
if account_id:
|
||||
try:
|
||||
from .models import Account
|
||||
account = Account.objects.get(id=account_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not account:
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
# Generate new access token
|
||||
access_token = generate_access_token(user, account)
|
||||
access_expires_at = get_token_expiry('access')
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
'access': access_token,
|
||||
'access_expires_at': access_expires_at.isoformat()
|
||||
},
|
||||
request=request
|
||||
)
|
||||
|
||||
except jwt.InvalidTokenError:
|
||||
return error_response(
|
||||
error='Invalid or expired refresh token',
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(exclude=True) # Exclude from public API documentation - internal authenticated endpoint
|
||||
class MeView(APIView):
|
||||
"""Get current user information."""
|
||||
@@ -201,6 +287,7 @@ urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
path('register/', csrf_exempt(RegisterView.as_view()), name='auth-register'),
|
||||
path('login/', csrf_exempt(LoginView.as_view()), name='auth-login'),
|
||||
path('refresh/', csrf_exempt(RefreshTokenView.as_view()), name='auth-refresh'),
|
||||
path('change-password/', ChangePasswordView.as_view(), name='auth-change-password'),
|
||||
path('me/', MeView.as_view(), name='auth-me'),
|
||||
]
|
||||
|
||||
@@ -933,12 +933,10 @@ class AuthViewSet(viewsets.GenericViewSet):
|
||||
return success_response(
|
||||
data={
|
||||
'user': user_serializer.data,
|
||||
'tokens': {
|
||||
'access': access_token,
|
||||
'refresh': refresh_token,
|
||||
'access_expires_at': access_expires_at.isoformat(),
|
||||
'refresh_expires_at': refresh_expires_at.isoformat(),
|
||||
}
|
||||
'access': access_token,
|
||||
'refresh': refresh_token,
|
||||
'access_expires_at': access_expires_at.isoformat(),
|
||||
'refresh_expires_at': refresh_expires_at.isoformat(),
|
||||
},
|
||||
message='Login successful',
|
||||
request=request
|
||||
|
||||
@@ -54,8 +54,8 @@ class CreditBalanceViewSet(viewsets.ViewSet):
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get plan credits per month
|
||||
plan_credits_per_month = account.plan.credits_per_month if account.plan else 0
|
||||
# Get plan credits per month (use get_effective_credits_per_month for Phase 0 compatibility)
|
||||
plan_credits_per_month = account.plan.get_effective_credits_per_month() if account.plan else 0
|
||||
|
||||
# Calculate credits used this month
|
||||
now = timezone.now()
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
# Generated manually for Phase 0: Module Enable Settings
|
||||
# Using RunSQL to create table directly to avoid model resolution issues with new unified API model
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('igny8_core_modules_system', '0006_alter_systemstatus_unique_together_and_more'),
|
||||
('system', '0006_alter_systemstatus_unique_together_and_more'),
|
||||
('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ModuleEnableSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('planner_enabled', models.BooleanField(default=True, help_text='Enable Planner module')),
|
||||
('writer_enabled', models.BooleanField(default=True, help_text='Enable Writer module')),
|
||||
('thinker_enabled', models.BooleanField(default=True, help_text='Enable Thinker module')),
|
||||
('automation_enabled', models.BooleanField(default=True, help_text='Enable Automation module')),
|
||||
('site_builder_enabled', models.BooleanField(default=True, help_text='Enable Site Builder module')),
|
||||
('linker_enabled', models.BooleanField(default=True, help_text='Enable Linker module')),
|
||||
('optimizer_enabled', models.BooleanField(default=True, help_text='Enable Optimizer module')),
|
||||
('publisher_enabled', models.BooleanField(default=True, help_text='Enable Publisher module')),
|
||||
('account', models.ForeignKey(on_delete=models.CASCADE, to='igny8_core_auth.account', db_column='tenant_id')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'igny8_module_enable_settings',
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='moduleenablesettings',
|
||||
constraint=models.UniqueConstraint(fields=('account',), name='unique_account_module_enable_settings'),
|
||||
# Create table using raw SQL to avoid model resolution issues
|
||||
# The model state is automatically discovered from models.py
|
||||
migrations.RunSQL(
|
||||
sql="""
|
||||
CREATE TABLE IF NOT EXISTS igny8_module_enable_settings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
planner_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
writer_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
thinker_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
automation_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
site_builder_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
linker_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
optimizer_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
publisher_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
tenant_id BIGINT NOT NULL REFERENCES igny8_tenants(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS igny8_module_enable_settings_tenant_id_idx ON igny8_module_enable_settings(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS igny8_module_enable_settings_account_created_idx ON igny8_module_enable_settings(tenant_id, created_at);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS unique_account_module_enable_settings ON igny8_module_enable_settings(tenant_id);
|
||||
""",
|
||||
reverse_sql="DROP TABLE IF EXISTS igny8_module_enable_settings CASCADE;",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -235,6 +235,15 @@ class ModuleSettingsViewSet(AccountModelViewSet):
|
||||
|
||||
def retrieve(self, request, pk=None):
|
||||
"""Get setting by key (pk can be key string)"""
|
||||
# Special case: if pk is "enable", this is likely a routing conflict
|
||||
# The correct endpoint is /settings/modules/enable/ which should go to ModuleEnableSettingsViewSet
|
||||
if pk == 'enable':
|
||||
return error_response(
|
||||
error='Use /api/v1/system/settings/modules/enable/ endpoint for module enable settings',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
|
||||
queryset = self.get_queryset()
|
||||
try:
|
||||
# Try to get by ID first
|
||||
@@ -301,7 +310,7 @@ class ModuleEnableSettingsViewSet(AccountModelViewSet):
|
||||
Allow read access to all authenticated users,
|
||||
but restrict write access to admins/owners
|
||||
"""
|
||||
if self.action in ['list', 'retrieve']:
|
||||
if self.action in ['list', 'retrieve', 'get_current']:
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
else:
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
|
||||
@@ -321,6 +330,14 @@ class ModuleEnableSettingsViewSet(AccountModelViewSet):
|
||||
queryset = queryset.filter(account=account)
|
||||
return queryset
|
||||
|
||||
@action(detail=False, methods=['get', 'put'], url_path='current', url_name='current')
|
||||
def get_current(self, request):
|
||||
"""Get or update current account's module enable settings"""
|
||||
if request.method == 'GET':
|
||||
return self.list(request)
|
||||
else:
|
||||
return self.update(request, pk=None)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Get or create module enable settings for current account"""
|
||||
try:
|
||||
@@ -337,15 +354,31 @@ class ModuleEnableSettingsViewSet(AccountModelViewSet):
|
||||
request=request
|
||||
)
|
||||
|
||||
# Get or create settings for account (one per account)
|
||||
# Check if table exists (migration might not have been run)
|
||||
try:
|
||||
settings = ModuleEnableSettings.objects.get(account=account)
|
||||
except ModuleEnableSettings.DoesNotExist:
|
||||
# Create default settings for account
|
||||
settings = ModuleEnableSettings.objects.create(account=account)
|
||||
# Get or create settings for account (one per account)
|
||||
try:
|
||||
settings = ModuleEnableSettings.objects.get(account=account)
|
||||
except ModuleEnableSettings.DoesNotExist:
|
||||
# Create default settings for account
|
||||
settings = ModuleEnableSettings.objects.create(account=account)
|
||||
|
||||
serializer = self.get_serializer(settings)
|
||||
return success_response(data=serializer.data, request=request)
|
||||
serializer = self.get_serializer(settings)
|
||||
return success_response(data=serializer.data, request=request)
|
||||
except Exception as db_error:
|
||||
# Check if it's a "table does not exist" error
|
||||
error_str = str(db_error)
|
||||
if 'does not exist' in error_str.lower() or 'relation' in error_str.lower():
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"ModuleEnableSettings table does not exist. Migration 0007_add_module_enable_settings needs to be run: {error_str}")
|
||||
return error_response(
|
||||
error='Module enable settings table not found. Please run migration: python manage.py migrate igny8_core_modules_system 0007',
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
request=request
|
||||
)
|
||||
# Re-raise other database errors
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_trace = traceback.format_exc()
|
||||
|
||||
@@ -16,8 +16,8 @@ router.register(r'strategies', StrategyViewSet, basename='strategy')
|
||||
router.register(r'settings/system', SystemSettingsViewSet, basename='system-settings')
|
||||
router.register(r'settings/account', AccountSettingsViewSet, basename='account-settings')
|
||||
router.register(r'settings/user', UserSettingsViewSet, basename='user-settings')
|
||||
# Register ModuleSettingsViewSet first
|
||||
router.register(r'settings/modules', ModuleSettingsViewSet, basename='module-settings')
|
||||
router.register(r'settings/modules/enable', ModuleEnableSettingsViewSet, basename='module-enable-settings')
|
||||
router.register(r'settings/ai', AISettingsViewSet, basename='ai-settings')
|
||||
|
||||
# Custom URL patterns for integration settings - matching reference plugin structure
|
||||
@@ -50,7 +50,20 @@ integration_image_gen_settings_viewset = IntegrationSettingsViewSet.as_view({
|
||||
'get': 'get_image_generation_settings',
|
||||
})
|
||||
|
||||
# Custom view for module enable settings to avoid URL routing conflict with ModuleSettingsViewSet
|
||||
# This must be defined as a custom path BEFORE router.urls to ensure it matches first
|
||||
# The update method handles pk=None correctly, so we can use as_view
|
||||
module_enable_viewset = ModuleEnableSettingsViewSet.as_view({
|
||||
'get': 'list',
|
||||
'put': 'update',
|
||||
'patch': 'partial_update',
|
||||
})
|
||||
|
||||
urlpatterns = [
|
||||
# Module enable settings endpoint - MUST come before router.urls to avoid conflict
|
||||
# When /settings/modules/enable/ is called, it would match ModuleSettingsViewSet with pk='enable'
|
||||
# So we define it as a custom path first
|
||||
path('settings/modules/enable/', module_enable_viewset, name='module-enable-settings'),
|
||||
path('', include(router.urls)),
|
||||
# Public health check endpoint (API Standard v1.0 requirement)
|
||||
path('ping/', ping, name='system-ping'),
|
||||
|
||||
@@ -411,9 +411,9 @@ frontend/
|
||||
<Route path="/reference/seed-keywords" element={<SeedKeywords />} />
|
||||
<Route path="/reference/industries" element={<ReferenceIndustries />} />
|
||||
|
||||
{/* Automation & Schedules */}
|
||||
{/* Automation */}
|
||||
<Route path="/automation" element={<AutomationDashboard />} />
|
||||
<Route path="/schedules" element={<Schedules />} />
|
||||
{/* Note: Schedules functionality is integrated into Automation Dashboard */}
|
||||
|
||||
{/* Settings */}
|
||||
<Route path="/settings" element={<GeneralSettings />} />
|
||||
|
||||
@@ -644,9 +644,12 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
||||
"data": {
|
||||
"user": { ... },
|
||||
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..."
|
||||
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||
"access_expires_at": "2025-01-XXT...",
|
||||
"refresh_expires_at": "2025-01-XXT..."
|
||||
},
|
||||
"message": "Login successful"
|
||||
"message": "Login successful",
|
||||
"request_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -278,11 +278,10 @@ frontend/src/
|
||||
│ ├── Billing/ # Existing
|
||||
│ ├── Settings/ # Existing
|
||||
│ ├── Automation/ # EXISTING (placeholder) - IMPLEMENT
|
||||
│ │ ├── Dashboard.tsx # Automation overview
|
||||
│ │ ├── Dashboard.tsx # Automation overview (includes schedules functionality)
|
||||
│ │ ├── Rules.tsx # Automation rules management
|
||||
│ │ ├── Workflows.tsx # Workflow templates
|
||||
│ │ └── History.tsx # Automation execution history
|
||||
│ ├── Schedules.tsx # EXISTING (placeholder) - IMPLEMENT
|
||||
│ ├── Linker/ # NEW
|
||||
│ │ ├── Dashboard.tsx
|
||||
│ │ ├── Candidates.tsx
|
||||
@@ -653,7 +652,7 @@ docker-data/
|
||||
| **Implement Automation Service** | `domain/automation/services/` | TODO | HIGH |
|
||||
| **Implement Automation API** | `modules/automation/` | TODO | HIGH |
|
||||
| **Implement Automation UI** | `frontend/src/pages/Automation/` | TODO | HIGH |
|
||||
| **Implement Schedules UI** | `frontend/src/pages/Schedules.tsx` | TODO | HIGH |
|
||||
| **Note**: Schedules functionality will be integrated into Automation UI, not as a separate page | - | - | - |
|
||||
|
||||
### 9.2 Phase 1: Site Builder
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ CREDIT_COSTS = {
|
||||
|------|-------|--------------|
|
||||
| **Automation Dashboard** | `frontend/src/pages/Automation/Dashboard.tsx` | EXISTING (placeholder) |
|
||||
| **Rules Management** | `frontend/src/pages/Automation/Rules.tsx` | NEW |
|
||||
| **Schedules Page** | `frontend/src/pages/Schedules.tsx` | EXISTING (placeholder) |
|
||||
| **Schedules (within Automation)** | Integrated into Automation Dashboard | Part of automation menu |
|
||||
| **Automation API Client** | `frontend/src/services/automation.api.ts` | NEW |
|
||||
|
||||
### 2.6 Testing
|
||||
|
||||
@@ -462,13 +462,11 @@ urlpatterns = router.urls
|
||||
- Test rule
|
||||
- Manual execution
|
||||
|
||||
#### Schedules Page
|
||||
#### Schedules (Part of Automation Menu)
|
||||
|
||||
| Task | File | Dependencies | Implementation |
|
||||
|------|------|--------------|----------------|
|
||||
| **Schedules Page** | `frontend/src/pages/Schedules.tsx` | EXISTING (placeholder) | View scheduled task history |
|
||||
**Note**: Schedules functionality will be integrated into the Automation menu group, not as a separate page.
|
||||
|
||||
**Schedules Page Features**:
|
||||
**Schedules Features** (within Automation Dashboard):
|
||||
- List scheduled tasks
|
||||
- Filter by status, rule, date
|
||||
- View execution results
|
||||
@@ -553,11 +551,11 @@ export const automationApi = {
|
||||
|
||||
- [ ] Implement `frontend/src/pages/Automation/Dashboard.tsx`
|
||||
- [ ] Create `frontend/src/pages/Automation/Rules.tsx`
|
||||
- [ ] Implement `frontend/src/pages/Schedules.tsx`
|
||||
- [ ] Integrate schedules functionality into Automation Dashboard (not as separate page)
|
||||
- [ ] Create `frontend/src/services/automation.api.ts`
|
||||
- [ ] Create rule creation wizard
|
||||
- [ ] Create rule editor
|
||||
- [ ] Create schedule history table
|
||||
- [ ] Create schedule history table (within Automation Dashboard)
|
||||
|
||||
### Testing Tasks
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ const SeedKeywords = lazy(() => import("./pages/Reference/SeedKeywords"));
|
||||
const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries"));
|
||||
|
||||
// Other Pages - Lazy loaded
|
||||
const Schedules = lazy(() => import("./pages/Schedules"));
|
||||
const AutomationDashboard = lazy(() => import("./pages/Automation/Dashboard"));
|
||||
|
||||
// Settings - Lazy loaded
|
||||
@@ -294,11 +293,6 @@ export default function App() {
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/schedules" element={
|
||||
<Suspense fallback={null}>
|
||||
<Schedules />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Settings */}
|
||||
<Route path="/settings" element={
|
||||
|
||||
@@ -21,7 +21,6 @@ import { useAuthStore } from "../../store/authStore";
|
||||
* - /settings (including /settings/sites)
|
||||
* - /dashboard
|
||||
* - /analytics
|
||||
* - /schedules
|
||||
* - /thinker
|
||||
* - /signin, /signup
|
||||
*/
|
||||
@@ -37,7 +36,6 @@ const SITE_SWITCHER_HIDDEN_PATHS = [
|
||||
'/settings',
|
||||
'/dashboard',
|
||||
'/analytics',
|
||||
'/schedules',
|
||||
'/thinker',
|
||||
];
|
||||
|
||||
|
||||
@@ -51,11 +51,6 @@ export const routes: RouteConfig[] = [
|
||||
{ path: '/thinker/profile', label: 'Profile', breadcrumb: 'Profile' },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/schedules',
|
||||
label: 'Schedules',
|
||||
icon: 'Schedules',
|
||||
},
|
||||
];
|
||||
|
||||
export const getBreadcrumbs = (pathname: string): Array<{ label: string; path: string }> => {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
PlugInIcon,
|
||||
TaskIcon,
|
||||
BoltIcon,
|
||||
TimeIcon,
|
||||
DocsIcon,
|
||||
PageIcon,
|
||||
DollarLineIcon,
|
||||
@@ -144,12 +143,6 @@ const AppSidebar: React.FC = () => {
|
||||
});
|
||||
}
|
||||
|
||||
workflowItems.push({
|
||||
icon: <TimeIcon />,
|
||||
name: "Schedules",
|
||||
path: "/schedules",
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
label: "OVERVIEW",
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function Help() {
|
||||
},
|
||||
{
|
||||
question: "How do I set up automation?",
|
||||
answer: "Go to Dashboard > Automation Setup section. Enable automation for each step (Keywords, Ideas, Content, Images) and configure settings like how many keywords to process per cycle. Advanced settings are available in Schedules page."
|
||||
answer: "Go to Dashboard > Automation Setup section. Enable automation for each step (Keywords, Ideas, Content, Images) and configure settings like how many keywords to process per cycle. Advanced scheduling settings are available in the Automation menu."
|
||||
},
|
||||
{
|
||||
question: "Can I edit AI-generated content?",
|
||||
@@ -539,7 +539,7 @@ export default function Help() {
|
||||
|
||||
<div className="mt-6 p-4 bg-brand-50 dark:bg-brand-900/10 rounded-lg border border-brand-200 dark:border-brand-800">
|
||||
<p className="text-sm text-brand-800 dark:text-brand-300">
|
||||
<strong>Note:</strong> Configure automation in Dashboard > Automation Setup. For advanced scheduling, go to Schedules page.
|
||||
<strong>Note:</strong> Configure automation in Dashboard > Automation Setup. For advanced scheduling, go to the Automation menu.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -194,13 +194,14 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
|
||||
|
||||
if (refreshResponse.ok) {
|
||||
const refreshData = await refreshResponse.json();
|
||||
if (refreshData.success && refreshData.access) {
|
||||
const accessToken = refreshData.data?.access || refreshData.access;
|
||||
if (refreshData.success && accessToken) {
|
||||
// Update token in store
|
||||
try {
|
||||
const authStorage = localStorage.getItem('auth-storage');
|
||||
if (authStorage) {
|
||||
const parsed = JSON.parse(authStorage);
|
||||
parsed.state.token = refreshData.access;
|
||||
parsed.state.token = accessToken;
|
||||
localStorage.setItem('auth-storage', JSON.stringify(parsed));
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -210,7 +211,7 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
|
||||
// Retry original request with new token
|
||||
const newHeaders = {
|
||||
...headers,
|
||||
'Authorization': `Bearer ${refreshData.access}`,
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
};
|
||||
|
||||
const retryResponse = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
|
||||
@@ -60,14 +60,17 @@ export const useAuthStore = create<AuthState>()(
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || 'Login failed');
|
||||
throw new Error(data.error || data.message || 'Login failed');
|
||||
}
|
||||
|
||||
// Store user and JWT tokens
|
||||
// Store user and JWT tokens (handle both old and new API formats)
|
||||
const responseData = data.data || data;
|
||||
// Support both formats: new (access/refresh at top level) and old (tokens.access/refresh)
|
||||
const tokens = responseData.tokens || {};
|
||||
set({
|
||||
user: data.user,
|
||||
token: data.tokens?.access || null,
|
||||
refreshToken: data.tokens?.refresh || null,
|
||||
user: responseData.user || data.user,
|
||||
token: responseData.access || tokens.access || data.access || null,
|
||||
refreshToken: responseData.refresh || tokens.refresh || data.refresh || null,
|
||||
isAuthenticated: true,
|
||||
loading: false
|
||||
});
|
||||
@@ -119,8 +122,8 @@ export const useAuthStore = create<AuthState>()(
|
||||
// Store user and JWT tokens
|
||||
set({
|
||||
user: data.user,
|
||||
token: data.tokens?.access || null,
|
||||
refreshToken: data.tokens?.refresh || null,
|
||||
token: data.data?.access || data.access || null,
|
||||
refreshToken: data.data?.refresh || data.refresh || null,
|
||||
isAuthenticated: true,
|
||||
loading: false
|
||||
});
|
||||
@@ -168,8 +171,8 @@ export const useAuthStore = create<AuthState>()(
|
||||
throw new Error(data.message || 'Token refresh failed');
|
||||
}
|
||||
|
||||
// Update access token
|
||||
set({ token: data.access });
|
||||
// Update access token (API returns access at top level of data)
|
||||
set({ token: data.data?.access || data.access });
|
||||
|
||||
// Also refresh user data to get latest account/plan information
|
||||
// This ensures account/plan changes are reflected immediately
|
||||
|
||||
Reference in New Issue
Block a user