Skip to content

Commit 89e2417

Browse files
committed
feat(auth): configure JWT issuer and expiry
- Added JWT issuer and expiry config to .env - Updated JWT service to use config values - Added TTL indexes for tokens and codes - Improved database seeding with new indexes - Configured CORS origin in environment
1 parent 85a289b commit 89e2417

File tree

4 files changed

+43
-10
lines changed

4 files changed

+43
-10
lines changed

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,12 @@
1515
# This allows the client (e.g., the HT Dashboard) to make requests to the API.
1616
# For local development, this can be left unset as 'localhost' is allowed by default.
1717
# CORS_ALLOWED_ORIGIN="https://your-dashboard.com"
18+
19+
# REQUIRED FOR PRODUCTION: The issuer URL for JWTs.
20+
# Defaults to 'http://localhost:8080' for local development if not set.
21+
# For production, this MUST be the public URL of your API.
22+
# JWT_ISSUER="https://api.your-domain.com"
23+
24+
# OPTIONAL: The expiry duration for JWTs in hours.
25+
# Defaults to 1 hour if not set.
26+
# JWT_EXPIRY_HOURS="24"

lib/src/config/environment_config.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,20 @@ abstract final class EnvironmentConfig {
108108
/// This is used to configure CORS for production environments.
109109
/// Returns `null` if the variable is not set.
110110
static String? get corsAllowedOrigin => _env['CORS_ALLOWED_ORIGIN'];
111+
112+
/// Retrieves the JWT issuer URL from the environment.
113+
///
114+
/// The value is read from the `JWT_ISSUER` environment variable.
115+
/// Defaults to 'http://localhost:8080' if not set.
116+
static String get jwtIssuer =>
117+
_env['JWT_ISSUER'] ?? 'http://localhost:8080';
118+
119+
/// Retrieves the JWT expiry duration in hours from the environment.
120+
///
121+
/// The value is read from the `JWT_EXPIRY_HOURS` environment variable.
122+
/// Defaults to 1 hour if not set or if parsing fails.
123+
static Duration get jwtExpiryDuration {
124+
final hours = int.tryParse(_env['JWT_EXPIRY_HOURS'] ?? '1');
125+
return Duration(hours: hours ?? 1);
126+
}
111127
}

lib/src/services/database_seeding_service.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,16 +145,27 @@ class DatabaseSeedingService {
145145
name: 'sources_text_index',
146146
);
147147

148+
// --- TTL and Unique Indexes via runCommand ---
149+
// The following indexes are created using the generic `runCommand` because
150+
// they require specific options not exposed by the simpler `createIndex`
151+
// helper method in the `mongo_dart` library.
152+
// Specifically, `expireAfterSeconds` is needed for TTL indexes.
153+
148154
// Indexes for the verification codes collection
149155
await _db.runCommand({
150156
'createIndexes': kVerificationCodesCollection,
151157
'indexes': [
152158
{
159+
// This is a TTL (Time-To-Live) index. MongoDB will automatically
160+
// delete documents from this collection when the `expiresAt` field's
161+
// value is older than the specified number of seconds (0).
153162
'key': {'expiresAt': 1},
154163
'name': 'expiresAt_ttl_index',
155164
'expireAfterSeconds': 0,
156165
},
157166
{
167+
// This ensures that each email can only have one pending
168+
// verification code at a time, preventing duplicates.
158169
'key': {'email': 1},
159170
'name': 'email_unique_index',
160171
'unique': true,
@@ -167,6 +178,8 @@ class DatabaseSeedingService {
167178
'createIndexes': kBlacklistedTokensCollection,
168179
'indexes': [
169180
{
181+
// This is a TTL index. MongoDB will automatically delete documents
182+
// (blacklisted tokens) when the `expiry` field's value is past.
170183
'key': {'expiry': 1},
171184
'name': 'expiry_ttl_index',
172185
'expireAfterSeconds': 0,

lib/src/services/jwt_auth_token_service.dart

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,30 @@ class JwtAuthTokenService implements AuthTokenService {
3232
final TokenBlacklistService _blacklistService;
3333
final Logger _log;
3434

35-
// --- Configuration ---
36-
37-
// Define token issuer and default expiry duration
38-
static const String _issuer = 'http://localhost:8080';
39-
static const Duration _tokenExpiryDuration = Duration(hours: 1);
40-
4135
// --- Interface Implementation ---
4236

4337
@override
4438
Future<String> generateToken(User user) async {
4539
try {
4640
final now = DateTime.now();
47-
final expiry = now.add(_tokenExpiryDuration);
41+
final expiry = now.add(EnvironmentConfig.jwtExpiryDuration);
42+
final issuer = EnvironmentConfig.jwtIssuer;
4843

4944
final jwt = JWT(
5045
{
5146
// Standard claims
5247
'sub': user.id, // Subject (user ID) - REQUIRED
5348
'exp': expiry.millisecondsSinceEpoch ~/ 1000, // Expiration Time
5449
'iat': now.millisecondsSinceEpoch ~/ 1000, // Issued At
55-
'iss': _issuer, // Issuer
50+
'iss': issuer, // Issuer
5651
'jti': ObjectId().oid, // JWT ID (for potential blacklisting)
5752
// Custom claims (optional, include what's useful)
5853
'email': user.email, // Kept for convenience
5954
// Embed the new enum-based roles. Use .name for string value.
6055
'appRole': user.appRole.name,
6156
'dashboardRole': user.dashboardRole.name,
6257
},
63-
issuer: _issuer,
58+
issuer: issuer,
6459
subject: user.id,
6560
jwtId: ObjectId().oid, // Re-setting jti here for clarity if needed
6661
);
@@ -69,7 +64,7 @@ class JwtAuthTokenService implements AuthTokenService {
6964
final token = jwt.sign(
7065
SecretKey(EnvironmentConfig.jwtSecretKey),
7166
algorithm: JWTAlgorithm.HS256,
72-
expiresIn: _tokenExpiryDuration, // Redundant but safe
67+
expiresIn: EnvironmentConfig.jwtExpiryDuration, // Redundant but safe
7368
);
7469

7570
_log.info('Generated JWT for user ${user.id}');

0 commit comments

Comments
 (0)