3 Commits

Author SHA1 Message Date
IGNY8 VPS (Salman)
56c30e4904 schedules page removed 2025-11-16 21:21:07 +00:00
IGNY8 VPS (Salman)
51cd021f85 fixed all phase 0 issues Enhance error handling for ModuleEnableSettings retrieval
- Added a check for the existence of the ModuleEnableSettings table before attempting to retrieve or fixed all phase 0 create settings for an account.
- Implemented logging and a user-friendly error response if the table does not exist, prompting the user to run the necessary migration.
- Updated migration to create the ModuleEnableSettings table using raw SQL to avoid model resolution issues.
2025-11-16 21:16:35 +00:00
IGNY8 VPS (Salman)
fc6dd5623a Add refresh token functionality and improve login response handling
- Introduced RefreshTokenView to allow users to refresh their access tokens using a valid refresh token.
- Enhanced LoginView to ensure correct user/account loading and improved error handling during user serialization.
- Updated API response structure to include access and refresh token expiration times.
- Adjusted frontend API handling to support both new and legacy token response formats.
2025-11-16 21:06:22 +00:00
18 changed files with 217 additions and 100 deletions

View File

@@ -14,8 +14,10 @@ from .views import (
SiteUserAccessViewSet, PlanViewSet, SiteViewSet, SectorViewSet, SiteUserAccessViewSet, PlanViewSet, SiteViewSet, SectorViewSet,
IndustryViewSet, SeedKeywordViewSet IndustryViewSet, SeedKeywordViewSet
) )
from .serializers import RegisterSerializer, LoginSerializer, ChangePasswordSerializer, UserSerializer from .serializers import RegisterSerializer, LoginSerializer, ChangePasswordSerializer, UserSerializer, RefreshTokenSerializer
from .models import User from .models import User
from .utils import generate_access_token, get_token_expiry, decode_token
import jwt
router = DefaultRouter() router = DefaultRouter()
# Main structure: Groups, Users, Accounts, Subscriptions, Site User Access # Main structure: Groups, Users, Accounts, Subscriptions, Site User Access
@@ -78,7 +80,7 @@ class LoginView(APIView):
password = serializer.validated_data['password'] password = serializer.validated_data['password']
try: try:
user = User.objects.get(email=email) user = User.objects.select_related('account', 'account__plan').get(email=email)
except User.DoesNotExist: except User.DoesNotExist:
return error_response( return error_response(
error='Invalid credentials', error='Invalid credentials',
@@ -107,9 +109,17 @@ class LoginView(APIView):
user_data = user_serializer.data user_data = user_serializer.data
except Exception as e: except Exception as e:
# Fallback if serializer fails (e.g., missing account_id column) # 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 = { user_data = {
'id': user.id, 'id': user.id,
'username': user.username, 'username': username,
'email': user.email, 'email': user.email,
'role': user.role, 'role': user.role,
'account': None, 'account': None,
@@ -119,12 +129,10 @@ class LoginView(APIView):
return success_response( return success_response(
data={ data={
'user': user_data, 'user': user_data,
'tokens': { 'access': access_token,
'access': access_token, 'refresh': refresh_token,
'refresh': refresh_token, 'access_expires_at': access_expires_at.isoformat(),
'access_expires_at': access_expires_at.isoformat(), 'refresh_expires_at': refresh_expires_at.isoformat(),
'refresh_expires_at': refresh_expires_at.isoformat(),
}
}, },
message='Login successful', message='Login successful',
request=request 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 @extend_schema(exclude=True) # Exclude from public API documentation - internal authenticated endpoint
class MeView(APIView): class MeView(APIView):
"""Get current user information.""" """Get current user information."""
@@ -201,6 +287,7 @@ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
path('register/', csrf_exempt(RegisterView.as_view()), name='auth-register'), path('register/', csrf_exempt(RegisterView.as_view()), name='auth-register'),
path('login/', csrf_exempt(LoginView.as_view()), name='auth-login'), 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('change-password/', ChangePasswordView.as_view(), name='auth-change-password'),
path('me/', MeView.as_view(), name='auth-me'), path('me/', MeView.as_view(), name='auth-me'),
] ]

View File

@@ -933,12 +933,10 @@ class AuthViewSet(viewsets.GenericViewSet):
return success_response( return success_response(
data={ data={
'user': user_serializer.data, 'user': user_serializer.data,
'tokens': { 'access': access_token,
'access': access_token, 'refresh': refresh_token,
'refresh': refresh_token, 'access_expires_at': access_expires_at.isoformat(),
'access_expires_at': access_expires_at.isoformat(), 'refresh_expires_at': refresh_expires_at.isoformat(),
'refresh_expires_at': refresh_expires_at.isoformat(),
}
}, },
message='Login successful', message='Login successful',
request=request request=request

View File

@@ -54,8 +54,8 @@ class CreditBalanceViewSet(viewsets.ViewSet):
request=request request=request
) )
# Get plan credits per month # Get plan credits per month (use get_effective_credits_per_month for Phase 0 compatibility)
plan_credits_per_month = account.plan.credits_per_month if account.plan else 0 plan_credits_per_month = account.plan.get_effective_credits_per_month() if account.plan else 0
# Calculate credits used this month # Calculate credits used this month
now = timezone.now() now = timezone.now()

View File

@@ -1,37 +1,39 @@
# Generated manually for Phase 0: Module Enable Settings # 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): class Migration(migrations.Migration):
dependencies = [ 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'), ('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
] ]
operations = [ operations = [
migrations.CreateModel( # Create table using raw SQL to avoid model resolution issues
name='ModuleEnableSettings', # The model state is automatically discovered from models.py
fields=[ migrations.RunSQL(
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), sql="""
('planner_enabled', models.BooleanField(default=True, help_text='Enable Planner module')), CREATE TABLE IF NOT EXISTS igny8_module_enable_settings (
('writer_enabled', models.BooleanField(default=True, help_text='Enable Writer module')), id BIGSERIAL PRIMARY KEY,
('thinker_enabled', models.BooleanField(default=True, help_text='Enable Thinker module')), planner_enabled BOOLEAN NOT NULL DEFAULT TRUE,
('automation_enabled', models.BooleanField(default=True, help_text='Enable Automation module')), writer_enabled BOOLEAN NOT NULL DEFAULT TRUE,
('site_builder_enabled', models.BooleanField(default=True, help_text='Enable Site Builder module')), thinker_enabled BOOLEAN NOT NULL DEFAULT TRUE,
('linker_enabled', models.BooleanField(default=True, help_text='Enable Linker module')), automation_enabled BOOLEAN NOT NULL DEFAULT TRUE,
('optimizer_enabled', models.BooleanField(default=True, help_text='Enable Optimizer module')), site_builder_enabled BOOLEAN NOT NULL DEFAULT TRUE,
('publisher_enabled', models.BooleanField(default=True, help_text='Enable Publisher module')), linker_enabled BOOLEAN NOT NULL DEFAULT TRUE,
('account', models.ForeignKey(on_delete=models.CASCADE, to='igny8_core_auth.account', db_column='tenant_id')), optimizer_enabled BOOLEAN NOT NULL DEFAULT TRUE,
], publisher_enabled BOOLEAN NOT NULL DEFAULT TRUE,
options={ tenant_id BIGINT NOT NULL REFERENCES igny8_tenants(id) ON DELETE CASCADE,
'db_table': 'igny8_module_enable_settings', created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
}, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
), );
migrations.AddConstraint( CREATE INDEX IF NOT EXISTS igny8_module_enable_settings_tenant_id_idx ON igny8_module_enable_settings(tenant_id);
model_name='moduleenablesettings', CREATE INDEX IF NOT EXISTS igny8_module_enable_settings_account_created_idx ON igny8_module_enable_settings(tenant_id, created_at);
constraint=models.UniqueConstraint(fields=('account',), name='unique_account_module_enable_settings'), 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;",
), ),
] ]

View File

@@ -235,6 +235,15 @@ class ModuleSettingsViewSet(AccountModelViewSet):
def retrieve(self, request, pk=None): def retrieve(self, request, pk=None):
"""Get setting by key (pk can be key string)""" """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() queryset = self.get_queryset()
try: try:
# Try to get by ID first # Try to get by ID first
@@ -301,7 +310,7 @@ class ModuleEnableSettingsViewSet(AccountModelViewSet):
Allow read access to all authenticated users, Allow read access to all authenticated users,
but restrict write access to admins/owners 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] permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
else: else:
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner] permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
@@ -321,6 +330,14 @@ class ModuleEnableSettingsViewSet(AccountModelViewSet):
queryset = queryset.filter(account=account) queryset = queryset.filter(account=account)
return queryset 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): def list(self, request, *args, **kwargs):
"""Get or create module enable settings for current account""" """Get or create module enable settings for current account"""
try: try:
@@ -337,15 +354,31 @@ class ModuleEnableSettingsViewSet(AccountModelViewSet):
request=request request=request
) )
# Get or create settings for account (one per account) # Check if table exists (migration might not have been run)
try: try:
settings = ModuleEnableSettings.objects.get(account=account) # Get or create settings for account (one per account)
except ModuleEnableSettings.DoesNotExist: try:
# Create default settings for account settings = ModuleEnableSettings.objects.get(account=account)
settings = ModuleEnableSettings.objects.create(account=account) except ModuleEnableSettings.DoesNotExist:
# Create default settings for account
serializer = self.get_serializer(settings) settings = ModuleEnableSettings.objects.create(account=account)
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: except Exception as e:
import traceback import traceback
error_trace = traceback.format_exc() error_trace = traceback.format_exc()

View File

@@ -16,8 +16,8 @@ router.register(r'strategies', StrategyViewSet, basename='strategy')
router.register(r'settings/system', SystemSettingsViewSet, basename='system-settings') router.register(r'settings/system', SystemSettingsViewSet, basename='system-settings')
router.register(r'settings/account', AccountSettingsViewSet, basename='account-settings') router.register(r'settings/account', AccountSettingsViewSet, basename='account-settings')
router.register(r'settings/user', UserSettingsViewSet, basename='user-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', ModuleSettingsViewSet, basename='module-settings')
router.register(r'settings/modules/enable', ModuleEnableSettingsViewSet, basename='module-enable-settings')
router.register(r'settings/ai', AISettingsViewSet, basename='ai-settings') router.register(r'settings/ai', AISettingsViewSet, basename='ai-settings')
# Custom URL patterns for integration settings - matching reference plugin structure # 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', '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 = [ 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)), path('', include(router.urls)),
# Public health check endpoint (API Standard v1.0 requirement) # Public health check endpoint (API Standard v1.0 requirement)
path('ping/', ping, name='system-ping'), path('ping/', ping, name='system-ping'),

View File

@@ -411,9 +411,9 @@ frontend/
<Route path="/reference/seed-keywords" element={<SeedKeywords />} /> <Route path="/reference/seed-keywords" element={<SeedKeywords />} />
<Route path="/reference/industries" element={<ReferenceIndustries />} /> <Route path="/reference/industries" element={<ReferenceIndustries />} />
{/* Automation & Schedules */} {/* Automation */}
<Route path="/automation" element={<AutomationDashboard />} /> <Route path="/automation" element={<AutomationDashboard />} />
<Route path="/schedules" element={<Schedules />} /> {/* Note: Schedules functionality is integrated into Automation Dashboard */}
{/* Settings */} {/* Settings */}
<Route path="/settings" element={<GeneralSettings />} /> <Route path="/settings" element={<GeneralSettings />} />

View File

@@ -644,9 +644,12 @@ class KeywordViewSet(SiteSectorModelViewSet):
"data": { "data": {
"user": { ... }, "user": { ... },
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...", "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"
} }
``` ```

View File

@@ -278,11 +278,10 @@ frontend/src/
│ ├── Billing/ # Existing │ ├── Billing/ # Existing
│ ├── Settings/ # Existing │ ├── Settings/ # Existing
│ ├── Automation/ # EXISTING (placeholder) - IMPLEMENT │ ├── Automation/ # EXISTING (placeholder) - IMPLEMENT
│ │ ├── Dashboard.tsx # Automation overview │ │ ├── Dashboard.tsx # Automation overview (includes schedules functionality)
│ │ ├── Rules.tsx # Automation rules management │ │ ├── Rules.tsx # Automation rules management
│ │ ├── Workflows.tsx # Workflow templates │ │ ├── Workflows.tsx # Workflow templates
│ │ └── History.tsx # Automation execution history │ │ └── History.tsx # Automation execution history
│ ├── Schedules.tsx # EXISTING (placeholder) - IMPLEMENT
│ ├── Linker/ # NEW │ ├── Linker/ # NEW
│ │ ├── Dashboard.tsx │ │ ├── Dashboard.tsx
│ │ ├── Candidates.tsx │ │ ├── Candidates.tsx
@@ -653,7 +652,7 @@ docker-data/
| **Implement Automation Service** | `domain/automation/services/` | TODO | HIGH | | **Implement Automation Service** | `domain/automation/services/` | TODO | HIGH |
| **Implement Automation API** | `modules/automation/` | TODO | HIGH | | **Implement Automation API** | `modules/automation/` | TODO | HIGH |
| **Implement Automation UI** | `frontend/src/pages/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 ### 9.2 Phase 1: Site Builder

View File

@@ -234,7 +234,7 @@ CREDIT_COSTS = {
|------|-------|--------------| |------|-------|--------------|
| **Automation Dashboard** | `frontend/src/pages/Automation/Dashboard.tsx` | EXISTING (placeholder) | | **Automation Dashboard** | `frontend/src/pages/Automation/Dashboard.tsx` | EXISTING (placeholder) |
| **Rules Management** | `frontend/src/pages/Automation/Rules.tsx` | NEW | | **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 | | **Automation API Client** | `frontend/src/services/automation.api.ts` | NEW |
### 2.6 Testing ### 2.6 Testing

View File

@@ -462,13 +462,11 @@ urlpatterns = router.urls
- Test rule - Test rule
- Manual execution - Manual execution
#### Schedules Page #### Schedules (Part of Automation Menu)
| Task | File | Dependencies | Implementation | **Note**: Schedules functionality will be integrated into the Automation menu group, not as a separate page.
|------|------|--------------|----------------|
| **Schedules Page** | `frontend/src/pages/Schedules.tsx` | EXISTING (placeholder) | View scheduled task history |
**Schedules Page Features**: **Schedules Features** (within Automation Dashboard):
- List scheduled tasks - List scheduled tasks
- Filter by status, rule, date - Filter by status, rule, date
- View execution results - View execution results
@@ -553,11 +551,11 @@ export const automationApi = {
- [ ] Implement `frontend/src/pages/Automation/Dashboard.tsx` - [ ] Implement `frontend/src/pages/Automation/Dashboard.tsx`
- [ ] Create `frontend/src/pages/Automation/Rules.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 `frontend/src/services/automation.api.ts`
- [ ] Create rule creation wizard - [ ] Create rule creation wizard
- [ ] Create rule editor - [ ] Create rule editor
- [ ] Create schedule history table - [ ] Create schedule history table (within Automation Dashboard)
### Testing Tasks ### Testing Tasks

View File

@@ -50,7 +50,6 @@ const SeedKeywords = lazy(() => import("./pages/Reference/SeedKeywords"));
const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries")); const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries"));
// Other Pages - Lazy loaded // Other Pages - Lazy loaded
const Schedules = lazy(() => import("./pages/Schedules"));
const AutomationDashboard = lazy(() => import("./pages/Automation/Dashboard")); const AutomationDashboard = lazy(() => import("./pages/Automation/Dashboard"));
// Settings - Lazy loaded // Settings - Lazy loaded
@@ -294,11 +293,6 @@ export default function App() {
</ModuleGuard> </ModuleGuard>
</Suspense> </Suspense>
} /> } />
<Route path="/schedules" element={
<Suspense fallback={null}>
<Schedules />
</Suspense>
} />
{/* Settings */} {/* Settings */}
<Route path="/settings" element={ <Route path="/settings" element={

View File

@@ -21,7 +21,6 @@ import { useAuthStore } from "../../store/authStore";
* - /settings (including /settings/sites) * - /settings (including /settings/sites)
* - /dashboard * - /dashboard
* - /analytics * - /analytics
* - /schedules
* - /thinker * - /thinker
* - /signin, /signup * - /signin, /signup
*/ */
@@ -37,7 +36,6 @@ const SITE_SWITCHER_HIDDEN_PATHS = [
'/settings', '/settings',
'/dashboard', '/dashboard',
'/analytics', '/analytics',
'/schedules',
'/thinker', '/thinker',
]; ];

View File

@@ -51,11 +51,6 @@ export const routes: RouteConfig[] = [
{ path: '/thinker/profile', label: 'Profile', breadcrumb: 'Profile' }, { path: '/thinker/profile', label: 'Profile', breadcrumb: 'Profile' },
], ],
}, },
{
path: '/schedules',
label: 'Schedules',
icon: 'Schedules',
},
]; ];
export const getBreadcrumbs = (pathname: string): Array<{ label: string; path: string }> => { export const getBreadcrumbs = (pathname: string): Array<{ label: string; path: string }> => {

View File

@@ -11,7 +11,6 @@ import {
PlugInIcon, PlugInIcon,
TaskIcon, TaskIcon,
BoltIcon, BoltIcon,
TimeIcon,
DocsIcon, DocsIcon,
PageIcon, PageIcon,
DollarLineIcon, DollarLineIcon,
@@ -144,12 +143,6 @@ const AppSidebar: React.FC = () => {
}); });
} }
workflowItems.push({
icon: <TimeIcon />,
name: "Schedules",
path: "/schedules",
});
return [ return [
{ {
label: "OVERVIEW", label: "OVERVIEW",

View File

@@ -76,7 +76,7 @@ export default function Help() {
}, },
{ {
question: "How do I set up automation?", question: "How do I set up automation?",
answer: "Go to Dashboard &gt; 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 &gt; 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?", 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"> <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"> <p className="text-sm text-brand-800 dark:text-brand-300">
<strong>Note:</strong> Configure automation in Dashboard &gt; Automation Setup. For advanced scheduling, go to Schedules page. <strong>Note:</strong> Configure automation in Dashboard &gt; Automation Setup. For advanced scheduling, go to the Automation menu.
</p> </p>
</div> </div>
</Card> </Card>

View File

@@ -194,13 +194,14 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
if (refreshResponse.ok) { if (refreshResponse.ok) {
const refreshData = await refreshResponse.json(); 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 // Update token in store
try { try {
const authStorage = localStorage.getItem('auth-storage'); const authStorage = localStorage.getItem('auth-storage');
if (authStorage) { if (authStorage) {
const parsed = JSON.parse(authStorage); const parsed = JSON.parse(authStorage);
parsed.state.token = refreshData.access; parsed.state.token = accessToken;
localStorage.setItem('auth-storage', JSON.stringify(parsed)); localStorage.setItem('auth-storage', JSON.stringify(parsed));
} }
} catch (e) { } catch (e) {
@@ -210,7 +211,7 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
// Retry original request with new token // Retry original request with new token
const newHeaders = { const newHeaders = {
...headers, ...headers,
'Authorization': `Bearer ${refreshData.access}`, 'Authorization': `Bearer ${accessToken}`,
}; };
const retryResponse = await fetch(`${API_BASE_URL}${endpoint}`, { const retryResponse = await fetch(`${API_BASE_URL}${endpoint}`, {

View File

@@ -60,14 +60,17 @@ export const useAuthStore = create<AuthState>()(
const data = await response.json(); const data = await response.json();
if (!response.ok || !data.success) { 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({ set({
user: data.user, user: responseData.user || data.user,
token: data.tokens?.access || null, token: responseData.access || tokens.access || data.access || null,
refreshToken: data.tokens?.refresh || null, refreshToken: responseData.refresh || tokens.refresh || data.refresh || null,
isAuthenticated: true, isAuthenticated: true,
loading: false loading: false
}); });
@@ -119,8 +122,8 @@ export const useAuthStore = create<AuthState>()(
// Store user and JWT tokens // Store user and JWT tokens
set({ set({
user: data.user, user: data.user,
token: data.tokens?.access || null, token: data.data?.access || data.access || null,
refreshToken: data.tokens?.refresh || null, refreshToken: data.data?.refresh || data.refresh || null,
isAuthenticated: true, isAuthenticated: true,
loading: false loading: false
}); });
@@ -168,8 +171,8 @@ export const useAuthStore = create<AuthState>()(
throw new Error(data.message || 'Token refresh failed'); throw new Error(data.message || 'Token refresh failed');
} }
// Update access token // Update access token (API returns access at top level of data)
set({ token: data.access }); set({ token: data.data?.access || data.access });
// Also refresh user data to get latest account/plan information // Also refresh user data to get latest account/plan information
// This ensures account/plan changes are reflected immediately // This ensures account/plan changes are reflected immediately