coregen-cli 3.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coregen.py +1585 -0
- coregen_cli-3.1.6.dist-info/METADATA +1211 -0
- coregen_cli-3.1.6.dist-info/RECORD +172 -0
- coregen_cli-3.1.6.dist-info/WHEEL +5 -0
- coregen_cli-3.1.6.dist-info/entry_points.txt +2 -0
- coregen_cli-3.1.6.dist-info/top_level.txt +2 -0
- generators/__init__.py +1 -0
- generators/assets/__init__.py +1 -0
- generators/assets/assets.py +30 -0
- generators/assets/main.py +4 -0
- generators/config/__init__.py +1 -0
- generators/config/analisy_options.py +42 -0
- generators/config/main.py +11 -0
- generators/config/pubspec.py +139 -0
- generators/cursor/__init__.py +3 -0
- generators/cursor/setup.py +87 -0
- generators/helpers/__init__.py +56 -0
- generators/helpers/component.py +1030 -0
- generators/helpers/config.py +255 -0
- generators/helpers/data_source.py +180 -0
- generators/helpers/domain.py +469 -0
- generators/helpers/feature.py +654 -0
- generators/helpers/navigation.py +274 -0
- generators/helpers/page.py +63 -0
- generators/helpers/project.py +33 -0
- generators/helpers/utils.py +338 -0
- generators/helpers/validation.py +364 -0
- generators/initializator.py +68 -0
- generators/main.py +81 -0
- generators/static/assets/logo.png +0 -0
- generators/static/assets/mock/auth_user.json +5 -0
- generators/static/assets/svgs/apple.svg +4 -0
- generators/static/assets/svgs/google.svg +2 -0
- generators/static/cursor/AGENTS.md.jinja +37 -0
- generators/static/cursor/agents/doc-writer.md +20 -0
- generators/static/cursor/agents/epic-orchestrator.md +28 -0
- generators/static/cursor/agents/feature-implementer.md +29 -0
- generators/static/cursor/agents/integration-wiring.md +30 -0
- generators/static/cursor/agents/layer-application.md +21 -0
- generators/static/cursor/agents/layer-guardian.md +21 -0
- generators/static/cursor/agents/layer-infrastructure.md +31 -0
- generators/static/cursor/agents/layer-model.md +24 -0
- generators/static/cursor/agents/layer-presentation.md +33 -0
- generators/static/cursor/docs/architecture/APIS_AND_INTEGRATION.md.jinja +42 -0
- generators/static/cursor/docs/architecture/CARAVAGGIO_COMPONENTS.md.jinja +317 -0
- generators/static/cursor/docs/architecture/DDD_LAYERS.md.jinja +32 -0
- generators/static/cursor/docs/architecture/FILE_TEMPLATES.md.jinja +76 -0
- generators/static/cursor/docs/architecture/MOCK_AND_REMOTE_DATA.md.jinja +87 -0
- generators/static/cursor/docs/architecture/REFERENCE_IMPLEMENTATIONS.md.jinja +48 -0
- generators/static/cursor/docs/architecture/WIDGETS_AND_CARAVAGGIO.md.jinja +76 -0
- generators/static/cursor/docs/epics/README.md +17 -0
- generators/static/cursor/lib/domain/AGENTS.md +23 -0
- generators/static/cursor/lib/features/AGENTS.md +23 -0
- generators/static/cursor/lib/widgets/AGENTS.md +21 -0
- generators/static/cursor/rules/apis-layer.mdc.jinja +21 -0
- generators/static/cursor/rules/application-layer.mdc.jinja +12 -0
- generators/static/cursor/rules/architecture-core.mdc.jinja +43 -0
- generators/static/cursor/rules/dart-conventions.mdc.jinja +14 -0
- generators/static/cursor/rules/domain-layer.mdc.jinja +24 -0
- generators/static/cursor/rules/login-context.mdc.jinja +16 -0
- generators/static/cursor/rules/presentation-layer.mdc.jinja +15 -0
- generators/static/cursor/rules/quality-gate.mdc.jinja +14 -0
- generators/static/cursor/rules/ui-caravaggio.mdc.jinja +22 -0
- generators/static/cursor/skills/caravaggio-ui/SKILL.md +138 -0
- generators/static/cursor/skills/epic-delivery/SKILL.md +74 -0
- generators/static/cursor/skills/epic-delivery/templates/epic-status.md +14 -0
- generators/static/cursor/skills/epic-delivery/templates/epic.md +36 -0
- generators/static/templates/apis/common/constants_template.jinja +48 -0
- generators/static/templates/apis/common/data_source_config_template.jinja +47 -0
- generators/static/templates/apis/core/api_injectable_module_template.jinja +14 -0
- generators/static/templates/apis/interceptors/api_logger_template.jinja +135 -0
- generators/static/templates/apis/interceptors/auth_interceptor_template.jinja +207 -0
- generators/static/templates/auth/application/auth_bloc_template.jinja +36 -0
- generators/static/templates/auth/application/auth_event_template.jinja +7 -0
- generators/static/templates/auth/application/auth_state_template.jinja +8 -0
- generators/static/templates/auth/infrastructure/auth_facade_module_template.jinja +16 -0
- generators/static/templates/auth/infrastructure/auth_facade_template.jinja +288 -0
- generators/static/templates/auth/infrastructure/mock_auth_facade_template.jinja +106 -0
- generators/static/templates/auth/model/auth_failure_template.jinja +11 -0
- generators/static/templates/auth/model/i_auth_facade_template.jinja +24 -0
- generators/static/templates/auth/model/user_template.jinja +34 -0
- generators/static/templates/auth/model/value_objects_template.jinja +54 -0
- generators/static/templates/auth/presentation/login_screen_template.jinja +24 -0
- generators/static/templates/auth/sign_in_form/application/sign_in_form_bloc_template.jinja +139 -0
- generators/static/templates/auth/sign_in_form/application/sign_in_form_event_template.jinja +15 -0
- generators/static/templates/auth/sign_in_form/application/sign_in_form_state_template.jinja +26 -0
- generators/static/templates/auth/sign_in_form/presentation/apple_form_template.jinja +16 -0
- generators/static/templates/auth/sign_in_form/presentation/email_password_form_template_INACTIVE.jinja +94 -0
- generators/static/templates/auth/sign_in_form/presentation/google_form_template.jinja +15 -0
- generators/static/templates/auth/sign_in_form/presentation/sign_in_form_template.jinja +381 -0
- generators/static/templates/component/component_bloc_template.jinja +49 -0
- generators/static/templates/component/component_event_template.jinja +7 -0
- generators/static/templates/component/component_form_bloc_template.jinja +56 -0
- generators/static/templates/component/component_form_event_template.jinja +7 -0
- generators/static/templates/component/component_form_state_template.jinja +18 -0
- generators/static/templates/component/component_form_widget_template.jinja +130 -0
- generators/static/templates/component/component_list_widget_template.jinja +95 -0
- generators/static/templates/component/component_state_template.jinja +9 -0
- generators/static/templates/component/component_widget_template.jinja +113 -0
- generators/static/templates/core/bloc/base_form_bloc_template.jinja +32 -0
- generators/static/templates/core/errors/error_localizer_template.jinja +61 -0
- generators/static/templates/core/infrastructure/base_mapper_template.jinja +21 -0
- generators/static/templates/core/infrastructure/base_repository_mixin_template.jinja +78 -0
- generators/static/templates/core/infrastructure/firebase_injectable_module_template.jinja +11 -0
- generators/static/templates/core/infrastructure/firestore_helpers_template.jinja +22 -0
- generators/static/templates/core/infrastructure/repository_error_handler_template.jinja +44 -0
- generators/static/templates/core/model/common_interfaces_template.jinja +3 -0
- generators/static/templates/core/model/entity_template.jinja +5 -0
- generators/static/templates/core/model/errors_template.jinja +15 -0
- generators/static/templates/core/model/failures_template.jinja +97 -0
- generators/static/templates/core/model/value_objects_template.jinja +135 -0
- generators/static/templates/core/model/value_validators_template.jinja +105 -0
- generators/static/templates/core/presentation/app_drawer_template.jinja +52 -0
- generators/static/templates/core/presentation/app_widget_template.jinja +41 -0
- generators/static/templates/core/presentation/bottom_nav_bar_template.jinja +32 -0
- generators/static/templates/domain/domain_mapper_template.jinja +39 -0
- generators/static/templates/domain/domain_remote_service_template.jinja +46 -0
- generators/static/templates/domain/domain_repository_template.jinja +90 -0
- generators/static/templates/domain/domain_service_module_template.jinja +23 -0
- generators/static/templates/domain/enum_template.jinja +3 -0
- generators/static/templates/domain/i_domain_service_template.jinja +18 -0
- generators/static/templates/domain/mock_domain_service_template.jinja +73 -0
- generators/static/templates/feature/feature_bloc_template.jinja +104 -0
- generators/static/templates/feature/feature_dto_template.jinja +26 -0
- generators/static/templates/feature/feature_entity_template.jinja +23 -0
- generators/static/templates/feature/feature_event_template.jinja +20 -0
- generators/static/templates/feature/feature_extensions_template.jinja +25 -0
- generators/static/templates/feature/feature_failure_template.jinja +11 -0
- generators/static/templates/feature/feature_page_template.jinja +113 -0
- generators/static/templates/feature/feature_repository_template.jinja +112 -0
- generators/static/templates/feature/feature_state_template.jinja +20 -0
- generators/static/templates/feature/i_feature_repository_template.jinja +21 -0
- generators/static/templates/feature/value_object_field_template.jinja +0 -0
- generators/static/templates/feature/value_object_template.jinja +0 -0
- generators/static/templates/feature/value_validators_template.jinja +57 -0
- generators/static/templates/home/home_screen_template.jinja +68 -0
- generators/static/templates/home/presentation/home_screen_template.jinja +52 -0
- generators/static/templates/home/screen_template.jinja +12 -0
- generators/static/templates/injection_template.jinja +8 -0
- generators/static/templates/logging/analytics_logging_template.jinja +42 -0
- generators/static/templates/logging/console_template.jinja +50 -0
- generators/static/templates/logging/logger_injectable_module_template.jinja +24 -0
- generators/static/templates/logging/logger_template.jinja +118 -0
- generators/static/templates/main_template.jinja +43 -0
- generators/static/templates/page_template.jinja +29 -0
- generators/static/templates/router_template.jinja +26 -0
- generators/static/templates/splash/presentation/splash_screen_auth_template.jinja +97 -0
- generators/static/templates/splash/presentation/splash_screen_template.jinja +51 -0
- generators/static/templates/storage/storage_repository_template.jinja +80 -0
- generators/static/templates/user_profile/infrastructure/user_profile_dto_template.jinja +27 -0
- generators/static/templates/user_profile/infrastructure/user_profile_mapper_template.jinja +43 -0
- generators/static/templates/user_profile/infrastructure/user_profile_repository_template.jinja +82 -0
- generators/static/templates/user_profile/infrastructure/user_profile_service_template.jinja +27 -0
- generators/static/templates/user_profile/model/i_user_profile_repository_template.jinja +34 -0
- generators/static/templates/user_profile/model/user_profile_failure_template.jinja +11 -0
- generators/static/templates/user_profile/model/user_profile_template.jinja +31 -0
- generators/static/templates/widgets/common/custom_scaffold_template.jinja +52 -0
- generators/static/templates/widgets/common/error_widget_template.jinja +22 -0
- generators/static/templates/widgets/common/loading_widget_template.jinja +14 -0
- generators/static/templates/widgets/common/unknown_state_widget_template.jinja +23 -0
- generators/templates/__init__.py +1 -0
- generators/templates/_core/core_generator.py +121 -0
- generators/templates/apis/apis_generator.py +50 -0
- generators/templates/auth/auth_generator.py +40 -0
- generators/templates/auth/sign_in_form_generator.py +22 -0
- generators/templates/copier.py +51 -0
- generators/templates/home/home_generator.py +26 -0
- generators/templates/lib/lib_generator.py +40 -0
- generators/templates/logging/logging_generator.py +28 -0
- generators/templates/main.py +75 -0
- generators/templates/splash/splash_generator.py +20 -0
- generators/templates/storage/storage_generator.py +5 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: epic-delivery
|
|
3
|
+
description: Decompose a high-level project description into epics and orchestrate implementation via subagents. Use when the user describes features, product scope, or asks to plan and build new functionality.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Epic delivery
|
|
7
|
+
|
|
8
|
+
Turn a high-level product description into implemented DDD features.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
Read before starting:
|
|
13
|
+
|
|
14
|
+
- Root `AGENTS.md`
|
|
15
|
+
- `docs/architecture/DDD_LAYERS.md`
|
|
16
|
+
- `docs/architecture/FILE_TEMPLATES.md`
|
|
17
|
+
- `docs/architecture/CARAVAGGIO_COMPONENTS.md` (when epic includes UI)
|
|
18
|
+
- `docs/architecture/MOCK_AND_REMOTE_DATA.md` (when epic includes domain entities or API)
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
### 1. Analyze
|
|
23
|
+
|
|
24
|
+
Understand the user's description. Identify domain entities, UI use cases, and dependencies between features.
|
|
25
|
+
|
|
26
|
+
### 2. Decompose into epics
|
|
27
|
+
|
|
28
|
+
Create one file per epic: `docs/epics/EPIC-NNN-<slug>.md` using template in `templates/epic.md`.
|
|
29
|
+
|
|
30
|
+
Each epic should be atomic: typically **one domain aggregate + one UI use case**.
|
|
31
|
+
|
|
32
|
+
Assign complexity tag:
|
|
33
|
+
|
|
34
|
+
| Tag | When |
|
|
35
|
+
|-----|------|
|
|
36
|
+
| `small` | 1 entity, 1 UI use case, no new API endpoints |
|
|
37
|
+
| `large` | Multiple entities, multiple screens, new Retrofit endpoints, or cross-feature components |
|
|
38
|
+
|
|
39
|
+
### 3. Order epics
|
|
40
|
+
|
|
41
|
+
Respect dependencies (e.g. auth before protected features). Implement independent epics sequentially or note parallelization in epic files.
|
|
42
|
+
|
|
43
|
+
### 4. Implement each epic
|
|
44
|
+
|
|
45
|
+
Delegate to subagent **`epic-orchestrator`** with:
|
|
46
|
+
|
|
47
|
+
- Epic file path
|
|
48
|
+
- Complexity tag (`small` | `large`)
|
|
49
|
+
|
|
50
|
+
**small** pipeline (~4 subagents):
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
feature-implementer → integration-wiring → doc-writer → layer-guardian
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**large** pipeline (~6–7 subagents):
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
layer-model → layer-infrastructure → layer-application → layer-presentation
|
|
60
|
+
→ integration-wiring → doc-writer → layer-guardian
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 5. Close epic
|
|
64
|
+
|
|
65
|
+
- Update epic file status (template: `templates/epic-status.md`)
|
|
66
|
+
- Run `flutter analyze`
|
|
67
|
+
- Address `layer-guardian` findings
|
|
68
|
+
|
|
69
|
+
## Constraints
|
|
70
|
+
|
|
71
|
+
- **Never** run `coregen` CLI — write Dart files directly.
|
|
72
|
+
- Do not duplicate widget policy — use `docs/architecture/WIDGETS_AND_CARAVAGGIO.md` and `docs/architecture/CARAVAGGIO_COMPONENTS.md`.
|
|
73
|
+
- For data layers use `docs/architecture/MOCK_AND_REMOTE_DATA.md` — default mock, create `assets/mock/<entity>.json`.
|
|
74
|
+
- Use Explore subagent for codebase search; Bash for analyze and build_runner.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## Completion log
|
|
2
|
+
|
|
3
|
+
| Step | Status | Notes |
|
|
4
|
+
|------|--------|-------|
|
|
5
|
+
| Domain model | | |
|
|
6
|
+
| Infrastructure | | |
|
|
7
|
+
| Application | | |
|
|
8
|
+
| Presentation | | |
|
|
9
|
+
| Integration wiring | | |
|
|
10
|
+
| Documentation | | |
|
|
11
|
+
| layer-guardian audit | | |
|
|
12
|
+
|
|
13
|
+
**Completed at:** _ISO date_
|
|
14
|
+
**Analyzer:** pass | fail
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# EPIC-[[epic_number]]: [[epic_title]]
|
|
2
|
+
|
|
3
|
+
**Status:** pending
|
|
4
|
+
**Complexity:** small | large
|
|
5
|
+
**Depends on:** none | EPIC-XXX
|
|
6
|
+
|
|
7
|
+
## Goal
|
|
8
|
+
|
|
9
|
+
[[epic_goal]]
|
|
10
|
+
|
|
11
|
+
## Domain
|
|
12
|
+
|
|
13
|
+
- Entity: `[[entity_name]]` → `lib/domain/[[entity_name]]/`
|
|
14
|
+
- Enums: [[enums_or_none]]
|
|
15
|
+
|
|
16
|
+
## Feature UI
|
|
17
|
+
|
|
18
|
+
- Feature: `[[feature_name]]` → `lib/features/[[feature_name]]/`
|
|
19
|
+
- Screens/components: [[ui_description]]
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
- New endpoints: yes | no
|
|
24
|
+
- Details: [[api_details]]
|
|
25
|
+
|
|
26
|
+
## Acceptance criteria
|
|
27
|
+
|
|
28
|
+
- [ ] Domain model and infrastructure complete
|
|
29
|
+
- [ ] BLoC and presentation complete
|
|
30
|
+
- [ ] Router, DI, error_localizer updated
|
|
31
|
+
- [ ] `flutter analyze` clean
|
|
32
|
+
- [ ] `layer-guardian` audit passed
|
|
33
|
+
|
|
34
|
+
## Files touched
|
|
35
|
+
|
|
36
|
+
_(updated by doc-writer on completion)_
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* API constants template for centralized configuration
|
|
3
|
+
* Provides a single place for all API-related constants:
|
|
4
|
+
* - Base API URL (configurable per environment)
|
|
5
|
+
* - Auth / token refresh base URL (often same as apiUrl or your IdP host)
|
|
6
|
+
* - Standard HTTP headers
|
|
7
|
+
* - API key management
|
|
8
|
+
*
|
|
9
|
+
* This ensures consistent API communication setup
|
|
10
|
+
* across all network requests in the application
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class Constants {
|
|
14
|
+
static const String _placeholderMarker = 'INSERT YOUR';
|
|
15
|
+
|
|
16
|
+
/// Main REST API base URL. Leave empty for mock-only development.
|
|
17
|
+
static String apiUrl = '';
|
|
18
|
+
|
|
19
|
+
/// Base URL for token refresh (e.g. Supabase auth). If empty, [AuthInterceptor] falls back to [dioBaseUrl].
|
|
20
|
+
static String authUrl = '';
|
|
21
|
+
|
|
22
|
+
/// True when [apiUrl] is a real URL (not empty or a generator placeholder).
|
|
23
|
+
static bool get isApiUrlConfigured {
|
|
24
|
+
final String url = apiUrl.trim();
|
|
25
|
+
if (url.isEmpty || url.contains(_placeholderMarker)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
final Uri? uri = Uri.tryParse(url);
|
|
29
|
+
return uri != null && uri.hasScheme && uri.host.isNotEmpty;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Base URL for Dio / Retrofit construction.
|
|
33
|
+
///
|
|
34
|
+
/// When [apiUrl] is not configured, returns a valid localhost URL so DI can
|
|
35
|
+
/// start while all entities use mock data (no HTTP calls are made).
|
|
36
|
+
static String get dioBaseUrl => isApiUrlConfigured ? apiUrl : 'http://127.0.0.1';
|
|
37
|
+
|
|
38
|
+
/// Sent as the `apikey` header on requests and refresh calls (set in your environment).
|
|
39
|
+
static const String apiKey = '';
|
|
40
|
+
|
|
41
|
+
static const String apiKeyHeader = "x-api-key";
|
|
42
|
+
/// Header name used by [AuthInterceptor] for the API key (matches common Supabase-style clients).
|
|
43
|
+
static const String apiKeyRequestHeader = "apikey";
|
|
44
|
+
|
|
45
|
+
static const String contentTypeHeader = "Content-type";
|
|
46
|
+
static const String acceptHeader = "Accept";
|
|
47
|
+
static const String authorizationHeader = "Authorization";
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Per-entity data source registry (mock vs remote).
|
|
3
|
+
*
|
|
4
|
+
* Change a single entry from DataSource.mock to DataSource.remote when the
|
|
5
|
+
* backend API for that entity is ready. New entities default to mock so the
|
|
6
|
+
* app runs without Constants.apiUrl.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import 'package:[[project_name]]/apis/common/constants.dart';
|
|
10
|
+
|
|
11
|
+
enum DataSource {
|
|
12
|
+
mock,
|
|
13
|
+
remote,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
abstract class DataSourceConfig {
|
|
17
|
+
/// Keys: snake_case entity folder name; 'auth' when login is enabled.
|
|
18
|
+
static const Map<String, DataSource> entities = {
|
|
19
|
+
[[entities_entries]]
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
static DataSource forEntity(String key) => entities[key] ?? DataSource.mock;
|
|
23
|
+
|
|
24
|
+
static bool useMock(String key) => forEntity(key) == DataSource.mock;
|
|
25
|
+
|
|
26
|
+
/// Throws if any non-auth entity uses remote but [Constants.apiUrl] is not configured.
|
|
27
|
+
static void validateRemoteRequiresApiUrl() {
|
|
28
|
+
if (Constants.isApiUrlConfigured) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
final List<String> remoteWithoutUrl = entities.entries
|
|
32
|
+
.where(
|
|
33
|
+
(MapEntry<String, DataSource> e) =>
|
|
34
|
+
e.value == DataSource.remote && e.key != 'auth',
|
|
35
|
+
)
|
|
36
|
+
.map((MapEntry<String, DataSource> e) => e.key)
|
|
37
|
+
.toList();
|
|
38
|
+
if (remoteWithoutUrl.isEmpty) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
throw StateError(
|
|
42
|
+
'DataSourceConfig: entity/entities ${remoteWithoutUrl.join(', ')} '
|
|
43
|
+
'use DataSource.remote but Constants.apiUrl is not configured. '
|
|
44
|
+
'Set apiUrl in initEnvironment() or switch to DataSource.mock.',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import 'package:dio/dio.dart';
|
|
2
|
+
import 'package:injectable/injectable.dart';
|
|
3
|
+
import 'package:[[project_name]]/apis/common/constants.dart';
|
|
4
|
+
import 'package:[[project_name]]/apis/interceptors/api_logger.dart';
|
|
5
|
+
import 'package:[[project_name]]/apis/interceptors/auth_interceptor.dart';
|
|
6
|
+
|
|
7
|
+
@module
|
|
8
|
+
abstract class ApisInjectableModule {
|
|
9
|
+
@lazySingleton
|
|
10
|
+
Dio get dio => Dio()
|
|
11
|
+
..options.baseUrl = Constants.dioBaseUrl
|
|
12
|
+
..interceptors.add(ApiLogger())
|
|
13
|
+
..interceptors.add(AuthInterceptor(Constants.dioBaseUrl));
|
|
14
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import 'package:dio/dio.dart';
|
|
2
|
+
import 'package:[[project_name]]/injection.dart';
|
|
3
|
+
import 'package:[[project_name]]/logging/logger.dart';
|
|
4
|
+
|
|
5
|
+
{% raw %}
|
|
6
|
+
/// Dio interceptor that logs requests/responses through [Logger] (apiReq / apiRes / warning / error).
|
|
7
|
+
///
|
|
8
|
+
/// Same idea as a classic OkHttp-style logger: toggles for request line, headers, body, response, errors.
|
|
9
|
+
class ApiLogger extends Interceptor {
|
|
10
|
+
ApiLogger({
|
|
11
|
+
this.showRequest = false,
|
|
12
|
+
this.request = false,
|
|
13
|
+
this.requestHeader = false,
|
|
14
|
+
this.requestBody = false,
|
|
15
|
+
this.responseHeader = false,
|
|
16
|
+
this.responseBody = false,
|
|
17
|
+
this.error = true,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
final bool showRequest;
|
|
21
|
+
|
|
22
|
+
/// Print request [Options]
|
|
23
|
+
bool request;
|
|
24
|
+
|
|
25
|
+
/// Print request header [Options.headers]
|
|
26
|
+
bool requestHeader;
|
|
27
|
+
|
|
28
|
+
/// Print request data [Options.data]
|
|
29
|
+
bool requestBody;
|
|
30
|
+
|
|
31
|
+
/// Print [Response.data] (via [Response.toString] when enabled)
|
|
32
|
+
bool responseBody;
|
|
33
|
+
|
|
34
|
+
/// Print [Response.headers]
|
|
35
|
+
bool responseHeader;
|
|
36
|
+
|
|
37
|
+
/// Print error message
|
|
38
|
+
bool error;
|
|
39
|
+
|
|
40
|
+
@override
|
|
41
|
+
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
|
42
|
+
if (showRequest) {
|
|
43
|
+
_printKV(options.method, options.uri);
|
|
44
|
+
}
|
|
45
|
+
if (request) {
|
|
46
|
+
_printKV(options.method, options.uri);
|
|
47
|
+
_printKV('method', options.method);
|
|
48
|
+
_printKV('responseType', options.responseType.toString());
|
|
49
|
+
_printKV('followRedirects', options.followRedirects);
|
|
50
|
+
_printKV('connectTimeout', options.connectTimeout);
|
|
51
|
+
_printKV('sendTimeout', options.sendTimeout);
|
|
52
|
+
_printKV('receiveTimeout', options.receiveTimeout);
|
|
53
|
+
_printKV('receiveDataWhenStatusError', options.receiveDataWhenStatusError);
|
|
54
|
+
_printKV('extra', options.extra);
|
|
55
|
+
}
|
|
56
|
+
if (requestHeader) {
|
|
57
|
+
_println('headers:');
|
|
58
|
+
options.headers.forEach((String key, dynamic v) => _printKV(' $key', v));
|
|
59
|
+
}
|
|
60
|
+
if (requestBody) {
|
|
61
|
+
_println('data:');
|
|
62
|
+
_printAll(options.data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
handler.next(options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@override
|
|
69
|
+
void onResponse(Response<dynamic> response, ResponseInterceptorHandler handler) {
|
|
70
|
+
_printResponse(response);
|
|
71
|
+
handler.next(response);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@override
|
|
75
|
+
void onError(DioException err, ErrorInterceptorHandler handler) {
|
|
76
|
+
if (error) {
|
|
77
|
+
final String method = err.requestOptions.method;
|
|
78
|
+
_println('$method Dio error:', statusCode: err.response?.statusCode);
|
|
79
|
+
if (err.response != null) {
|
|
80
|
+
_printResponse(err.response!);
|
|
81
|
+
}
|
|
82
|
+
_println('$err', statusCode: err.response?.statusCode);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
handler.next(err);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
void _printResponse(Response<dynamic> response) {
|
|
89
|
+
final int? statusCode = response.statusCode;
|
|
90
|
+
final String method = response.requestOptions.method;
|
|
91
|
+
final String statusText = statusCode?.toString() ?? 'No status';
|
|
92
|
+
_printKV('$method $statusText', response.requestOptions.uri, statusCode: statusCode);
|
|
93
|
+
if (responseHeader) {
|
|
94
|
+
_printKV('statusCode', statusCode, statusCode: statusCode);
|
|
95
|
+
if (response.isRedirect == true) {
|
|
96
|
+
_printKV('redirect', response.realUri, statusCode: statusCode);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_println('headers:', statusCode: statusCode);
|
|
100
|
+
response.headers.forEach(
|
|
101
|
+
(String key, List<String> v) => _printKV(' $key', v.join('\r\n\t'), statusCode: statusCode),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (responseBody) {
|
|
105
|
+
_println('Response Text:', statusCode: statusCode);
|
|
106
|
+
_printAll(response.toString(), statusCode: statusCode);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
void _printKV(String key, Object? v, {int? statusCode}) {
|
|
111
|
+
_println('$key: $v', statusCode: statusCode);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
void _println(String value, {int? statusCode}) {
|
|
115
|
+
final Logger logger = getIt<Logger>();
|
|
116
|
+
if (statusCode != null) {
|
|
117
|
+
if (statusCode > 399 && statusCode < 600) {
|
|
118
|
+
if (statusCode == 401) {
|
|
119
|
+
logger.logWarning(value);
|
|
120
|
+
} else {
|
|
121
|
+
logger.logError(value);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
logger.logApiRes(value);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
logger.logApiReq(value);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
void _printAll(Object? msg, {int? statusCode}) {
|
|
132
|
+
msg.toString().split('\n').forEach((String x) => _println(x, statusCode: statusCode));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
{% endraw %}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Authentication interceptor for API requests (Potluck-style):
|
|
3
|
+
* - Guest mode: x-guest-token, clears Authorization
|
|
4
|
+
* - User mode: Bearer access token, clears x-guest-token
|
|
5
|
+
* - apikey + x-locale headers
|
|
6
|
+
* - On 401: refresh via POST to /token?grant_type=refresh_token (Supabase-style), retry once, persist tokens
|
|
7
|
+
* - Optional [AuthBloc] sign-out when refresh fails (generated only with `create --login`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import 'dart:ui';
|
|
11
|
+
|
|
12
|
+
import 'package:dio/dio.dart';
|
|
13
|
+
import 'package:[[project_name]]/apis/common/constants.dart';
|
|
14
|
+
import 'package:[[project_name]]/injection.dart';
|
|
15
|
+
import 'package:[[project_name]]/storage/storage_repository.dart';
|
|
16
|
+
{% if has_login %}
|
|
17
|
+
import 'package:[[project_name]]/features/auth/application/auth_bloc.dart';
|
|
18
|
+
{% endif %}
|
|
19
|
+
|
|
20
|
+
/// Queued interceptor: bearer / guest headers + refresh on 401.
|
|
21
|
+
class AuthInterceptor extends QueuedInterceptor {
|
|
22
|
+
AuthInterceptor(String baseUrl)
|
|
23
|
+
: dioAuth = Dio()
|
|
24
|
+
..options.baseUrl =
|
|
25
|
+
Constants.authUrl.isNotEmpty ? Constants.authUrl : baseUrl,
|
|
26
|
+
dio = Dio()..options.baseUrl = baseUrl;
|
|
27
|
+
|
|
28
|
+
final Dio dioAuth;
|
|
29
|
+
final Dio dio;
|
|
30
|
+
|
|
31
|
+
{% if has_login %}
|
|
32
|
+
AuthBloc? _getAuthBloc() {
|
|
33
|
+
try {
|
|
34
|
+
return getIt<AuthBloc>();
|
|
35
|
+
} on Object catch (_) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
void _triggerSignOut() {
|
|
41
|
+
final AuthBloc? authBloc = _getAuthBloc();
|
|
42
|
+
if (authBloc != null) {
|
|
43
|
+
authBloc.add(const AuthEvent.signedOut());
|
|
44
|
+
} else {
|
|
45
|
+
Future<void>.delayed(const Duration(milliseconds: 100), () {
|
|
46
|
+
final AuthBloc? retry = _getAuthBloc();
|
|
47
|
+
retry?.add(const AuthEvent.signedOut());
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
{% else %}
|
|
52
|
+
void _triggerSignOut() {
|
|
53
|
+
// Auth UI not generated (create without --login). Clear tokens only, or wire your own sign-out.
|
|
54
|
+
}
|
|
55
|
+
{% endif %}
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
|
59
|
+
final StorageRepository storageRepo = getIt<StorageRepository>();
|
|
60
|
+
bool isGuest = await storageRepo.isGuestMode();
|
|
61
|
+
String? guestToken = await storageRepo.getGuestToken();
|
|
62
|
+
|
|
63
|
+
if (guestToken != null && guestToken.isNotEmpty && !isGuest) {
|
|
64
|
+
await storageRepo.saveGuestToken(guestToken);
|
|
65
|
+
isGuest = true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
final bool shouldUseGuestMode = isGuest || (guestToken != null && guestToken.isNotEmpty);
|
|
69
|
+
|
|
70
|
+
if (shouldUseGuestMode) {
|
|
71
|
+
options.headers.remove(Constants.authorizationHeader);
|
|
72
|
+
if (guestToken != null && guestToken.isNotEmpty) {
|
|
73
|
+
options.headers['x-guest-token'] = guestToken;
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
final String? accessToken = await storageRepo.getToken();
|
|
77
|
+
if (accessToken != null && accessToken.isNotEmpty) {
|
|
78
|
+
options.headers.remove('x-guest-token');
|
|
79
|
+
options.headers[Constants.authorizationHeader] = 'Bearer $accessToken';
|
|
80
|
+
} else {
|
|
81
|
+
options.headers.remove(Constants.authorizationHeader);
|
|
82
|
+
guestToken = await storageRepo.getGuestToken();
|
|
83
|
+
if (guestToken != null && guestToken.isNotEmpty) {
|
|
84
|
+
options.headers['x-guest-token'] = guestToken;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Constants.apiKey.isNotEmpty) {
|
|
90
|
+
options.headers[Constants.apiKeyRequestHeader] = Constants.apiKey;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
final String deviceLocale = PlatformDispatcher.instance.locale.languageCode;
|
|
94
|
+
final String locale =
|
|
95
|
+
(deviceLocale == 'it' || deviceLocale == 'en') ? deviceLocale : 'en';
|
|
96
|
+
options.headers['x-locale'] = locale;
|
|
97
|
+
|
|
98
|
+
handler.next(options);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@override
|
|
102
|
+
void onResponse(Response<dynamic> response, ResponseInterceptorHandler handler) {
|
|
103
|
+
response.extra = <String, dynamic>{};
|
|
104
|
+
handler.next(response);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@override
|
|
108
|
+
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
|
109
|
+
if (err.response?.statusCode == 401) {
|
|
110
|
+
final StorageRepository storage = getIt<StorageRepository>();
|
|
111
|
+
final bool isGuest = await storage.isGuestMode();
|
|
112
|
+
if (isGuest) {
|
|
113
|
+
handler.next(err);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
final String? refreshToken = await storage.getRefreshToken();
|
|
119
|
+
if (refreshToken == null || refreshToken.isEmpty) {
|
|
120
|
+
await storage.removeToken();
|
|
121
|
+
_triggerSignOut();
|
|
122
|
+
handler.next(err);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
final Response<dynamic> refreshResponse = await dioAuth.post<dynamic>(
|
|
127
|
+
'/token?grant_type=refresh_token',
|
|
128
|
+
data: <String, dynamic>{'refresh_token': refreshToken},
|
|
129
|
+
options: Options(
|
|
130
|
+
headers: <String, dynamic>{
|
|
131
|
+
Constants.apiKeyRequestHeader: Constants.apiKey,
|
|
132
|
+
Constants.contentTypeHeader: 'application/json',
|
|
133
|
+
},
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
final Object? refreshBodyRaw = refreshResponse.data;
|
|
138
|
+
if (refreshBodyRaw is! Map<dynamic, dynamic>) {
|
|
139
|
+
await storage.removeToken();
|
|
140
|
+
_triggerSignOut();
|
|
141
|
+
handler.next(err);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
final Map<String, dynamic> refreshBody =
|
|
145
|
+
Map<String, dynamic>.from(refreshBodyRaw);
|
|
146
|
+
final String? newToken = refreshBody['access_token'] as String?;
|
|
147
|
+
final String? newRefresh = refreshBody['refresh_token'] as String?;
|
|
148
|
+
|
|
149
|
+
if (newToken == null || newToken.isEmpty) {
|
|
150
|
+
await storage.removeToken();
|
|
151
|
+
_triggerSignOut();
|
|
152
|
+
handler.next(err);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
final Map<String, dynamic> headers =
|
|
157
|
+
Map<String, dynamic>.from(err.requestOptions.headers);
|
|
158
|
+
headers[Constants.authorizationHeader] = 'Bearer $newToken';
|
|
159
|
+
if (Constants.apiKey.isNotEmpty) {
|
|
160
|
+
headers[Constants.apiKeyRequestHeader] = Constants.apiKey;
|
|
161
|
+
}
|
|
162
|
+
headers[Constants.contentTypeHeader] = 'application/json';
|
|
163
|
+
|
|
164
|
+
final String deviceLocale = PlatformDispatcher.instance.locale.languageCode;
|
|
165
|
+
final String locale =
|
|
166
|
+
(deviceLocale == 'it' || deviceLocale == 'en') ? deviceLocale : 'en';
|
|
167
|
+
headers['x-locale'] = locale;
|
|
168
|
+
|
|
169
|
+
final Options options = Options(
|
|
170
|
+
method: err.requestOptions.method,
|
|
171
|
+
headers: headers,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
final Response<dynamic> retryResponse = await dio.request<dynamic>(
|
|
175
|
+
err.requestOptions.path,
|
|
176
|
+
data: err.requestOptions.data,
|
|
177
|
+
queryParameters: err.requestOptions.queryParameters,
|
|
178
|
+
options: options,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (retryResponse.statusCode != 401) {
|
|
182
|
+
await storage.saveToken(
|
|
183
|
+
TokenData(
|
|
184
|
+
token: newToken,
|
|
185
|
+
refreshToken: (newRefresh != null && newRefresh.isNotEmpty)
|
|
186
|
+
? newRefresh
|
|
187
|
+
: refreshToken,
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
handler.resolve(retryResponse);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await storage.removeToken();
|
|
195
|
+
_triggerSignOut();
|
|
196
|
+
handler.reject(err);
|
|
197
|
+
return;
|
|
198
|
+
} on Object catch (_) {
|
|
199
|
+
await storage.removeToken();
|
|
200
|
+
_triggerSignOut();
|
|
201
|
+
handler.next(err);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
handler.next(err);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import 'package:bloc/bloc.dart';
|
|
2
|
+
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
3
|
+
import 'package:[[project_name]]/domain/auth/model/user.dart';
|
|
4
|
+
import 'package:[[project_name]]/domain/auth/model/i_auth_facade.dart';
|
|
5
|
+
import 'package:dartz/dartz.dart';
|
|
6
|
+
|
|
7
|
+
part 'auth_event.dart';
|
|
8
|
+
part 'auth_state.dart';
|
|
9
|
+
|
|
10
|
+
part 'auth_bloc.freezed.dart';
|
|
11
|
+
|
|
12
|
+
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|
13
|
+
final IAuthFacade _authFacade;
|
|
14
|
+
|
|
15
|
+
AuthBloc(this._authFacade) : super(const AuthState.initial()) {
|
|
16
|
+
on<AuthCheckRequested>(_onAuthCheckRequested);
|
|
17
|
+
on<SignedOut>(_onSignedOut);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Future<void> _onAuthCheckRequested(AuthCheckRequested event, Emitter<AuthState> emit) async {
|
|
21
|
+
final Option<User> userOption = await _authFacade.getSignedInUser();
|
|
22
|
+
userOption.fold(
|
|
23
|
+
() {
|
|
24
|
+
emit(const AuthState.unauthenticated());
|
|
25
|
+
},
|
|
26
|
+
(User user) {
|
|
27
|
+
emit(AuthState.authenticated(user));
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Future<void> _onSignedOut(SignedOut event, Emitter<AuthState> emit) async {
|
|
33
|
+
await _authFacade.signOut();
|
|
34
|
+
emit(const AuthState.unauthenticated());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
part of 'auth_bloc.dart';
|
|
2
|
+
|
|
3
|
+
@freezed
|
|
4
|
+
abstract class AuthState with _$AuthState {
|
|
5
|
+
const factory AuthState.initial() = Initial;
|
|
6
|
+
const factory AuthState.authenticated(User user) = Authenticated;
|
|
7
|
+
const factory AuthState.unauthenticated() = Unauthenticated;
|
|
8
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import 'package:injectable/injectable.dart';
|
|
2
|
+
import 'package:[[project_name]]/apis/common/data_source_config.dart';
|
|
3
|
+
import 'package:[[project_name]]/domain/auth/infrastructure/auth_facade.dart';
|
|
4
|
+
import 'package:[[project_name]]/domain/auth/infrastructure/mock_auth_facade.dart';
|
|
5
|
+
import 'package:[[project_name]]/domain/auth/model/i_auth_facade.dart';
|
|
6
|
+
|
|
7
|
+
@module
|
|
8
|
+
abstract class AuthFacadeModule {
|
|
9
|
+
@lazySingleton
|
|
10
|
+
IAuthFacade authFacade(AuthFacade remote, MockAuthFacade mock) {
|
|
11
|
+
if (DataSourceConfig.useMock('auth')) {
|
|
12
|
+
return mock;
|
|
13
|
+
}
|
|
14
|
+
return remote;
|
|
15
|
+
}
|
|
16
|
+
}
|