Skip to content

Refactor sync with new models api #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 48 commits into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
c09c3b3
refactor: ensures the RBAC system's permission definitions are in syn…
fulleni Jul 10, 2025
dd5af06
refactor: Replaces the old string-based role-to-permission mapping wi…
fulleni Jul 10, 2025
753aaf9
fix(rbac): correct constant name in role permissions map
fulleni Jul 10, 2025
4e79d3a
refactor(rbac): enhance permission checking logic
fulleni Jul 10, 2025
7f64912
feat(auth): update JWT claims for enum-based roles
fulleni Jul 10, 2025
3af92e3
refactor(auth): upgrade anonymous user linking process
fulleni Jul 10, 2025
f038b3a
fix(auth): update user role checks and registration output
fulleni Jul 10, 2025
2832ed5
refactor(dashboard): replace Category with Topic in DashboardSummaryS…
fulleni Jul 10, 2025
cdb98e0
refactor(user-preferences): update limit enforcement logic and depend…
fulleni Jul 10, 2025
587cb6e
feat(database): update database schema for new features
fulleni Jul 10, 2025
b7954ed
refactor(db): update seeding process and SQL queries
fulleni Jul 10, 2025
33cab08
refactor(database): update seeding process and data handling
fulleni Jul 10, 2025
ef5ee16
refactor(dependencies): simplify repository configurations
fulleni Jul 10, 2025
0adbfcf
fix(authorization): update permissions and model names
fulleni Jul 10, 2025
3973c35
refactor(auth): replace print statements with logging for better trac…
fulleni Jul 10, 2025
91d0f6f
feat(user-preference): inject log into user preference limit service
fulleni Jul 10, 2025
f524e4f
refactor(inmemory): improve code formatting in app_dependencies.dart
fulleni Jul 10, 2025
bc95a9e
refactor(token_blacklist_service): replace print statements with logging
fulleni Jul 10, 2025
d42156e
feat(auth): add logging to token blacklist service
fulleni Jul 10, 2025
b915b5d
refactor(auth): replace print statements with logging in SimpleAuthTo…
fulleni Jul 10, 2025
46550e1
docs(README): update Key Capabilities section with more detail and cl…
fulleni Jul 10, 2025
980324a
feat(service): add logger to DefaultUserPreferenceLimitService
fulleni Jul 10, 2025
c98bc5f
lint: misc
fulleni Jul 10, 2025
53e8a32
fix(deps): resolve linter warnings and compile errors
fulleni Jul 10, 2025
e6c4e6b
refactor(service): replace print statements with structured logging
fulleni Jul 10, 2025
6a82d95
refactor(dependencies): simplify JSON deserialization functions
fulleni Jul 10, 2025
40a8019
fix(auth): correct user creation and deletion logic
fulleni Jul 10, 2025
d0b8ff8
fix(db): correct type errors and lints in seeding service
fulleni Jul 10, 2025
d56d246
fix(db): correct type errors and lints in seeding service
fulleni Jul 10, 2025
e700c64
fix(database): update remote config JSON key and improve related code
fulleni Jul 10, 2025
638c7c4
refactor(api): use camelCase for isDashboardLogin in request-code
fulleni Jul 10, 2025
347b347
refactor(api): use camelCase for isDashboardLogin in verify-code
fulleni Jul 10, 2025
221f5d1
refactor(api): update link-email check to use user.appRole
fulleni Jul 10, 2025
8ceb050
refactor(api): update verify-link-email check to use user.appRole
fulleni Jul 10, 2025
c8ac366
refactor(api): update root providers for Topic and RemoteConfig models
fulleni Jul 10, 2025
ece92ee
refactor(api): align generic data item route with new models
fulleni Jul 10, 2025
5046c4b
feat(api): add response metadata to anonymous auth endpoint
fulleni Jul 10, 2025
fe9055d
feat(api): add response metadata to verify-code endpoint
fulleni Jul 10, 2025
39bc43f
feat(api): add response metadata to verify-link-email endpoint
fulleni Jul 10, 2025
97ea137
fix(api): correct requestId handling in /auth/me endpoint
fulleni Jul 10, 2025
8069022
fix(api): synchronize data route with Topic model rename
fulleni Jul 10, 2025
92e75fe
fix(api): synchronize data route with RemoteConfig model rename
fulleni Jul 10, 2025
d429b17
docs(api): update stale comment in data route test cases
fulleni Jul 10, 2025
72428e6
fix(api): enforce camelCase for error codes in responses
fulleni Jul 10, 2025
0d666c4
docs: add remote configuration to key capabilities in README
fulleni Jul 10, 2025
885379e
refactor(jwt_auth_token_service): optimize logging in validateToken a…
fulleni Jul 10, 2025
471c9ed
refactor(config): remove use of test-only member in EnvironmentConfig
fulleni Jul 10, 2025
228396e
style: format
fulleni Jul 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,37 @@ management dashboard](https://github.com/headlines-toolkit/ht-dashboard).
## ✨ Key Capabilities

* 🔒 **Flexible & Secure Authentication:** Provide seamless user access with
a unified system supporting passwordless sign-in, anonymous guest
accounts, and a secure, context-aware login flow for privileged dashboard
users (e.g., 'admin', 'publisher').
a unified system supporting passwordless email sign-in, anonymous guest
accounts, and a secure, role-aware login flow for privileged dashboard
users.

* ⚡️ **Flexible Role-Based Access Control (RBAC):** Implement granular
permissions with a flexible, multi-role system. Assign multiple roles to
users (e.g., 'admin', 'publisher', 'premium_user') to precisely control
access to different API features and data management capabilities.
* ⚡️ **Granular Role-Based Access Control (RBAC):** Implement precise
permissions with a dual-role system (`appRole` for application features,
`dashboardRole` for admin functions) to control access to API features
and data management capabilities.

* ⚙️ **Synchronized App Settings:** Ensure a consistent and personalized user
experience across devices by effortlessly syncing application preferences
like theme, language, font styles, and more.

* 👤 **Personalized User Preferences:** Enable richer user interactions by
managing and syncing user-specific data such as saved headlines, followed sources, or other personalized content tailored to individual users.
managing and syncing user-specific data such as saved headlines, followed
sources, and followed topics tailored to individual users.

* 💾 **Robust Data Management:** Securely manage core news data (headlines,
categories, sources) through a well-structured API that supports flexible
topics, sources) through a well-structured API that supports flexible
querying and sorting for dynamic content presentation.

* 🌐 **Dynamic Remote Configuration:** Centrally manage application
behavior—including ad frequency, feature flags, and maintenance status—without
requiring a client-side update.

* 💾 **Robust Data Management:** Securely manage core news data (headlines,
topics, sources) through a well-structured API that supports flexible
querying and sorting for dynamic content presentation.
Comment on lines +45 to 47

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This section seems to be a duplicate of the 'Robust Data Management' section above (lines 37-39). To improve clarity, this duplicated block should be removed.


* 📊 **Dynamic Dashboard Summary:** Access real-time, aggregated metrics on
key data points like total headlines, categories, and sources, providing
key data points like total headlines, topics, and sources, providing
an at-a-glance overview for administrative dashboards.

* 🔧 **Solid Technical Foundation:** Built with Dart and the high-performance
Expand Down
234 changes: 100 additions & 134 deletions lib/src/config/app_dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,54 @@ class AppDependencies {
final _completer = Completer<void>();

// --- Repositories ---
/// A repository for managing [Headline] data.
late final HtDataRepository<Headline> headlineRepository;
late final HtDataRepository<Category> categoryRepository;

/// A repository for managing [Topic] data.
late final HtDataRepository<Topic> topicRepository;

/// A repository for managing [Source] data.
late final HtDataRepository<Source> sourceRepository;

/// A repository for managing [Country] data.
late final HtDataRepository<Country> countryRepository;

/// A repository for managing [User] data.
late final HtDataRepository<User> userRepository;

/// A repository for managing [UserAppSettings] data.
late final HtDataRepository<UserAppSettings> userAppSettingsRepository;

/// A repository for managing [UserContentPreferences] data.
late final HtDataRepository<UserContentPreferences>
userContentPreferencesRepository;
late final HtDataRepository<AppConfig> appConfigRepository;

/// A repository for managing the global [RemoteConfig] data.
late final HtDataRepository<RemoteConfig> remoteConfigRepository;

// --- Services ---
/// A service for sending emails.
late final HtEmailRepository emailRepository;

/// A service for managing a blacklist of invalidated authentication tokens.
late final TokenBlacklistService tokenBlacklistService;

/// A service for generating and validating authentication tokens.
late final AuthTokenService authTokenService;

/// A service for storing and validating one-time verification codes.
late final VerificationCodeStorageService verificationCodeStorageService;

/// A service that orchestrates authentication logic.
late final AuthService authService;

/// A service for calculating and providing a summary for the dashboard.
late final DashboardSummaryService dashboardSummaryService;

/// A service for checking user permissions.
late final PermissionService permissionService;

/// A service for enforcing limits on user content preferences.
late final UserPreferenceLimitService userPreferenceLimitService;

/// Initializes all application dependencies.
Expand Down Expand Up @@ -100,189 +130,107 @@ class AppDependencies {
headlineRepository = _createRepository(
connection,
'headlines',
(json) {
if (json['created_at'] is DateTime) {
json['created_at'] =
(json['created_at'] as DateTime).toIso8601String();
}
if (json['updated_at'] is DateTime) {
json['updated_at'] =
(json['updated_at'] as DateTime).toIso8601String();
}
if (json['published_at'] is DateTime) {
json['published_at'] =
(json['published_at'] as DateTime).toIso8601String();
}
return Headline.fromJson(json);
},
(headline) {
final json = headline.toJson();
// The database expects source_id and category_id, not nested objects.
// We extract the IDs and remove the original objects to match the
// schema.
if (headline.source != null) {
json['source_id'] = headline.source!.id;
}
if (headline.category != null) {
json['category_id'] = headline.category!.id;
}
json.remove('source');
json.remove('category');
return json;
},
// The HtDataPostgresClient returns DateTime objects from TIMESTAMPTZ
// columns. The Headline.fromJson factory expects ISO 8601 strings.
// This handler converts them before deserialization.
(json) => Headline.fromJson(_convertTimestampsToString(json)),
(headline) => headline.toJson()
..['source_id'] = headline.source.id
..['topic_id'] = headline.topic.id
..['event_country_id'] = headline.eventCountry.id
..remove('source')
..remove('topic')
..remove('eventCountry'),
);
categoryRepository = _createRepository(
topicRepository = _createRepository(
connection,
'categories',
(json) {
if (json['created_at'] is DateTime) {
json['created_at'] =
(json['created_at'] as DateTime).toIso8601String();
}
if (json['updated_at'] is DateTime) {
json['updated_at'] =
(json['updated_at'] as DateTime).toIso8601String();
}
return Category.fromJson(json);
},
(c) => c.toJson(),
'topics',
(json) => Topic.fromJson(_convertTimestampsToString(json)),
(topic) => topic.toJson(),
);
sourceRepository = _createRepository(
connection,
'sources',
(json) {
if (json['created_at'] is DateTime) {
json['created_at'] =
(json['created_at'] as DateTime).toIso8601String();
}
if (json['updated_at'] is DateTime) {
json['updated_at'] =
(json['updated_at'] as DateTime).toIso8601String();
}
return Source.fromJson(json);
},
(source) {
final json = source.toJson();
// The database expects headquarters_country_id, not a nested object.
// We extract the ID and remove the original object to match the
// schema.
json['headquarters_country_id'] = source.headquarters?.id;
json.remove('headquarters');
return json;
},
(json) => Source.fromJson(_convertTimestampsToString(json)),
(source) => source.toJson()
..['headquarters_country_id'] = source.headquarters.id
..remove('headquarters'),
);
countryRepository = _createRepository(
connection,
'countries',
(json) {
if (json['created_at'] is DateTime) {
json['created_at'] =
(json['created_at'] as DateTime).toIso8601String();
}
if (json['updated_at'] is DateTime) {
json['updated_at'] =
(json['updated_at'] as DateTime).toIso8601String();
}
return Country.fromJson(json);
},
(c) => c.toJson(),
(json) => Country.fromJson(_convertTimestampsToString(json)),
(country) => country.toJson(),
);
userRepository = _createRepository(
connection,
'users',
(json) {
// The postgres driver returns DateTime objects, but the model's
// fromJson expects ISO 8601 strings. We must convert them first.
if (json['created_at'] is DateTime) {
json['created_at'] = (json['created_at'] as DateTime).toIso8601String();
}
if (json['last_engagement_shown_at'] is DateTime) {
json['last_engagement_shown_at'] =
(json['last_engagement_shown_at'] as DateTime).toIso8601String();
}
return User.fromJson(json);
},
(json) => User.fromJson(_convertTimestampsToString(json)),
(user) {
// The `roles` field is a List<String>, but the database expects a
// JSONB array. We must explicitly encode it.
final json = user.toJson();
json['roles'] = jsonEncode(json['roles']);
// Convert enums to their string names for the database.
json['app_role'] = user.appRole.name;
json['dashboard_role'] = user.dashboardRole.name;
// The `feed_action_status` map must be JSON encoded for the JSONB column.
json['feed_action_status'] = jsonEncode(json['feed_action_status']);
return json;
},
);
userAppSettingsRepository = _createRepository(
connection,
'user_app_settings',
(json) {
// The DB has created_at/updated_at, but the model doesn't.
// Remove them before deserialization to avoid CheckedFromJsonException.
json.remove('created_at');
json.remove('updated_at');
return UserAppSettings.fromJson(json);
},
UserAppSettings.fromJson,
(settings) {
final json = settings.toJson();
// These fields are complex objects and must be JSON encoded for the DB.
json['display_settings'] = jsonEncode(json['display_settings']);
json['feed_preferences'] = jsonEncode(json['feed_preferences']);
json['engagement_shown_counts'] =
jsonEncode(json['engagement_shown_counts']);
json['engagement_last_shown_timestamps'] =
jsonEncode(json['engagement_last_shown_timestamps']);
return json;
},
);
userContentPreferencesRepository = _createRepository(
connection,
'user_content_preferences',
(json) {
// The postgres driver returns DateTime objects, but the model's
// fromJson expects ISO 8601 strings. We must convert them first.
if (json['created_at'] is DateTime) {
json['created_at'] =
(json['created_at'] as DateTime).toIso8601String();
}
if (json['updated_at'] is DateTime) {
json['updated_at'] =
(json['updated_at'] as DateTime).toIso8601String();
}
return UserContentPreferences.fromJson(json);
},
UserContentPreferences.fromJson,
(preferences) {
final json = preferences.toJson();
json['followed_categories'] = jsonEncode(json['followed_categories']);
// These fields are lists of complex objects and must be JSON encoded.
json['followed_topics'] = jsonEncode(json['followed_topics']);
json['followed_sources'] = jsonEncode(json['followed_sources']);
json['followed_countries'] = jsonEncode(json['followed_countries']);
json['saved_headlines'] = jsonEncode(json['saved_headlines']);
return json;
},
);
appConfigRepository = _createRepository(
remoteConfigRepository = _createRepository(
connection,
'app_config',
(json) {
if (json['created_at'] is DateTime) {
json['created_at'] =
(json['created_at'] as DateTime).toIso8601String();
}
if (json['updated_at'] is DateTime) {
json['updated_at'] =
(json['updated_at'] as DateTime).toIso8601String();
}
return AppConfig.fromJson(json);
'remote_config',
(json) => RemoteConfig.fromJson(_convertTimestampsToString(json)),
(config) {
final json = config.toJson();
// All nested config objects must be JSON encoded for JSONB columns.
json['user_preference_limits'] = jsonEncode(
json['user_preference_limits'],
);
json['ad_config'] = jsonEncode(json['ad_config']);
json['account_action_config'] = jsonEncode(
json['account_action_config'],
);
json['app_status'] = jsonEncode(json['app_status']);
return json;
},
(c) => c.toJson(),
);

// 4. Initialize Services.
emailRepository = const HtEmailRepository(
emailClient: HtEmailInMemoryClient(),
);
tokenBlacklistService = InMemoryTokenBlacklistService();
tokenBlacklistService = InMemoryTokenBlacklistService(log: _log);
authTokenService = JwtAuthTokenService(
userRepository: userRepository,
blacklistService: tokenBlacklistService,
uuidGenerator: const Uuid(),
log: _log,
);
verificationCodeStorageService = InMemoryVerificationCodeStorageService();
authService = AuthService(
Expand All @@ -293,15 +241,17 @@ class AppDependencies {
userAppSettingsRepository: userAppSettingsRepository,
userContentPreferencesRepository: userContentPreferencesRepository,
uuidGenerator: const Uuid(),
log: _log,
);
dashboardSummaryService = DashboardSummaryService(
headlineRepository: headlineRepository,
categoryRepository: categoryRepository,
topicRepository: topicRepository,
sourceRepository: sourceRepository,
);
permissionService = const PermissionService();
userPreferenceLimitService = DefaultUserPreferenceLimitService(
appConfigRepository: appConfigRepository,
remoteConfigRepository: remoteConfigRepository,
log: _log,
);
}

Expand All @@ -321,4 +271,20 @@ class AppDependencies {
),
);
}

/// Converts DateTime values in a JSON map to ISO 8601 strings.
///
/// The postgres driver returns DateTime objects for TIMESTAMPTZ columns,
/// but our models' `fromJson` factories expect ISO 8601 strings. This
/// utility function performs the conversion for known timestamp fields.
Map<String, dynamic> _convertTimestampsToString(Map<String, dynamic> json) {
const timestampKeys = {'created_at', 'updated_at'};
final newJson = Map<String, dynamic>.from(json);
for (final key in timestampKeys) {
if (newJson[key] is DateTime) {
newJson[key] = (newJson[key] as DateTime).toIso8601String();
}
}
return newJson;
}
}
Loading
Loading