refactor(android)!: restructure to standard Capacitor plugin layout
Restructure Android project from nested module layout to standard Capacitor plugin structure following community conventions. Structure Changes: - Move plugin code from android/plugin/ to android/src/main/java/ - Move test app from android/app/ to test-apps/android-test-app/app/ - Remove nested android/plugin module structure - Remove nested android/app test app structure Build Infrastructure: - Add Gradle wrapper files (gradlew, gradlew.bat, gradle/wrapper/) - Transform android/build.gradle from root project to library module - Update android/settings.gradle for standalone plugin builds - Add android/gradle.properties with AndroidX configuration - Add android/consumer-rules.pro for ProGuard rules Configuration Updates: - Add prepare script to package.json for automatic builds on npm install - Update package.json version to 1.0.1 - Update android/build.gradle to properly resolve Capacitor dependencies - Update test-apps/android-test-app/settings.gradle with correct paths - Remove android/variables.gradle (hardcode values in build.gradle) Documentation: - Update BUILDING.md with new structure and build process - Update INTEGRATION_GUIDE.md to reflect standard structure - Update README.md to remove path fix warnings - Add test-apps/BUILD_PROCESS.md documenting test app build flows Test App Configuration: - Fix android-test-app to correctly reference plugin and Capacitor - Remove capacitor-cordova-android-plugins dependency (not needed) - Update capacitor.settings.gradle path verification in fix script BREAKING CHANGE: Plugin now uses standard Capacitor Android structure. Consuming apps must update their capacitor.settings.gradle to reference android/ instead of android/plugin/. This is automatically handled by Capacitor CLI for apps using standard plugin installation.
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* DailyNotificationJWTManager.java
|
||||
*
|
||||
* Android JWT Manager for TimeSafari authentication enhancement
|
||||
* Extends existing ETagManager infrastructure with DID-based JWT authentication
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
* @created 2025-10-03 06:53:30 UTC
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.util.Log;
|
||||
import android.content.Context;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.MessageDigest;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Manages JWT authentication for TimeSafari integration
|
||||
*
|
||||
* This class extends the existing ETagManager infrastructure by adding:
|
||||
* - DID-based JWT token generation
|
||||
* - Automatic JWT header injection into HTTP requests
|
||||
* - JWT token expiration management
|
||||
* - Integration with existing DailyNotificationETagManager
|
||||
*
|
||||
* Phase 1 Implementation: Extends existing DailyNotificationETagManager.java
|
||||
*/
|
||||
public class DailyNotificationJWTManager {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private static final String TAG = "DailyNotificationJWTManager";
|
||||
|
||||
// JWT Headers
|
||||
private static final String HEADER_AUTHORIZATION = "Authorization";
|
||||
private static final String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
|
||||
// JWT Configuration
|
||||
private static final int DEFAULT_JWT_EXPIRATION_SECONDS = 60;
|
||||
|
||||
// JWT Algorithm (simplified for Phase 1)
|
||||
private static final String ALGORITHM = "HS256";
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private final DailyNotificationStorage storage;
|
||||
private final DailyNotificationETagManager eTagManager;
|
||||
|
||||
// Current authentication state
|
||||
private String currentActiveDid;
|
||||
private String currentJWTToken;
|
||||
private long jwtExpirationTime;
|
||||
|
||||
// Configuration
|
||||
private int jwtExpirationSeconds;
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param storage Storage instance for persistence
|
||||
* @param eTagManager ETagManager instance for HTTP enhancements
|
||||
*/
|
||||
public DailyNotificationJWTManager(DailyNotificationStorage storage, DailyNotificationETagManager eTagManager) {
|
||||
this.storage = storage;
|
||||
this.eTagManager = eTagManager;
|
||||
this.jwtExpirationSeconds = DEFAULT_JWT_EXPIRATION_SECONDS;
|
||||
|
||||
Log.d(TAG, "JWTManager initialized with ETagManager integration");
|
||||
}
|
||||
|
||||
// MARK: - ActiveDid Management
|
||||
|
||||
/**
|
||||
* Set the active DID for authentication
|
||||
*
|
||||
* @param activeDid The DID to use for JWT generation
|
||||
*/
|
||||
public void setActiveDid(String activeDid) {
|
||||
setActiveDid(activeDid, DEFAULT_JWT_EXPIRATION_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active DID for authentication with custom expiration
|
||||
*
|
||||
* @param activeDid The DID to use for JWT generation
|
||||
* @param expirationSeconds JWT expiration time in seconds
|
||||
*/
|
||||
public void setActiveDid(String activeDid, int expirationSeconds) {
|
||||
try {
|
||||
Log.d(TAG, "Setting activeDid: " + activeDid + " with " + expirationSeconds + "s expiration");
|
||||
|
||||
this.currentActiveDid = activeDid;
|
||||
this.jwtExpirationSeconds = expirationSeconds;
|
||||
|
||||
// Generate new JWT token immediately
|
||||
generateAndCacheJWT();
|
||||
|
||||
Log.i(TAG, "ActiveDid set successfully");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error setting activeDid", e);
|
||||
throw new RuntimeException("Failed to set activeDid", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current active DID
|
||||
*
|
||||
* @return Current active DID or null if not set
|
||||
*/
|
||||
public String getCurrentActiveDid() {
|
||||
return currentActiveDid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have a valid active DID and JWT token
|
||||
*
|
||||
* @return true if authentication is ready
|
||||
*/
|
||||
public boolean isAuthenticated() {
|
||||
return currentActiveDid != null &&
|
||||
currentJWTToken != null &&
|
||||
!isTokenExpired();
|
||||
}
|
||||
|
||||
// MARK: - JWT Token Management
|
||||
|
||||
/**
|
||||
* Generate JWT token for current activeDid
|
||||
*
|
||||
* @param expiresInSeconds Expiration time in seconds
|
||||
* @return Generated JWT token
|
||||
*/
|
||||
public String generateJWTForActiveDid(String activeDid, int expiresInSeconds) {
|
||||
try {
|
||||
Log.d(TAG, "Generating JWT for activeDid: " + activeDid);
|
||||
|
||||
long currentTime = System.currentTimeMillis() / 1000;
|
||||
|
||||
// Create JWT payload
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("exp", currentTime + expiresInSeconds);
|
||||
payload.put("iat", currentTime);
|
||||
payload.put("iss", activeDid);
|
||||
payload.put("aud", "timesafari.notifications");
|
||||
payload.put("sub", activeDid);
|
||||
|
||||
// Generate JWT token (simplified implementation for Phase 1)
|
||||
String jwt = signWithDID(payload, activeDid);
|
||||
|
||||
Log.d(TAG, "JWT generated successfully");
|
||||
return jwt;
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error generating JWT", e);
|
||||
throw new RuntimeException("Failed to generate JWT", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and cache JWT token for current activeDid
|
||||
*/
|
||||
private void generateAndCacheJWT() {
|
||||
if (currentActiveDid == null) {
|
||||
Log.w(TAG, "Cannot generate JWT: no activeDid set");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentJWTToken = generateJWTForActiveDid(currentActiveDid, jwtExpirationSeconds);
|
||||
jwtExpirationTime = System.currentTimeMillis() + (jwtExpirationSeconds * 1000L);
|
||||
|
||||
Log.d(TAG, "JWT cached successfully, expires at: " + jwtExpirationTime);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error caching JWT", e);
|
||||
throw new RuntimeException("Failed to cache JWT", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current JWT token is expired
|
||||
*
|
||||
* @return true if token is expired
|
||||
*/
|
||||
private boolean isTokenExpired() {
|
||||
return currentJWTToken == null || System.currentTimeMillis() >= jwtExpirationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh JWT token if needed
|
||||
*/
|
||||
public void refreshJWTIfNeeded() {
|
||||
if (isTokenExpired()) {
|
||||
Log.d(TAG, "JWT token expired, refreshing");
|
||||
generateAndCacheJWT();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current valid JWT token (refreshes if needed)
|
||||
*
|
||||
* @return Current JWT token
|
||||
*/
|
||||
public String getCurrentJWTToken() {
|
||||
refreshJWTIfNeeded();
|
||||
return currentJWTToken;
|
||||
}
|
||||
|
||||
// MARK: - HTTP Client Enhancement
|
||||
|
||||
/**
|
||||
* Enhance HTTP client with JWT authentication headers
|
||||
*
|
||||
* Extends existing DailyNotificationETagManager connection creation
|
||||
*
|
||||
* @param connection HTTP connection to enhance
|
||||
* @param activeDid DID for authentication (optional, uses current if null)
|
||||
*/
|
||||
public void enhanceHttpClientWithJWT(HttpURLConnection connection, String activeDid) {
|
||||
try {
|
||||
// Set activeDid if provided
|
||||
if (activeDid != null && !activeDid.equals(currentActiveDid)) {
|
||||
setActiveDid(activeDid);
|
||||
}
|
||||
|
||||
// Ensure we have a valid token
|
||||
if (!isAuthenticated()) {
|
||||
throw new IllegalStateException("No valid authentication available");
|
||||
}
|
||||
|
||||
// Add JWT Authorization header
|
||||
String jwt = getCurrentJWTToken();
|
||||
connection.setRequestProperty(HEADER_AUTHORIZATION, "Bearer " + jwt);
|
||||
|
||||
// Set JSON content type for API requests
|
||||
connection.setRequestProperty(HEADER_CONTENT_TYPE, "application/json");
|
||||
|
||||
Log.d(TAG, "HTTP client enhanced with JWT authentication");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error enhancing HTTP client with JWT", e);
|
||||
throw new RuntimeException("Failed to enhance HTTP client", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance HTTP client with JWT authentication for current activeDid
|
||||
*
|
||||
* @param connection HTTP connection to enhance
|
||||
*/
|
||||
public void enhanceHttpClientWithJWT(HttpURLConnection connection) {
|
||||
enhanceHttpClientWithJWT(connection, null);
|
||||
}
|
||||
|
||||
// MARK: - JWT Signing (Simplified for Phase 1)
|
||||
|
||||
/**
|
||||
* Sign JWT payload with DID (simplified implementation)
|
||||
*
|
||||
* Phase 1: Basic implementation using DID-based signing
|
||||
* Later phases: Integrate with proper DID cryptography
|
||||
*
|
||||
* @param payload JWT payload
|
||||
* @param did DID for signing
|
||||
* @return Signed JWT token
|
||||
*/
|
||||
private String signWithDID(Map<String, Object> payload, String did) {
|
||||
try {
|
||||
// Phase 1: Simplified JWT implementation
|
||||
// In production, this would use proper DID + cryptography libraries
|
||||
|
||||
// Create JWT header
|
||||
Map<String, Object> header = new HashMap<>();
|
||||
header.put("alg", ALGORITHM);
|
||||
header.put("typ", "JWT");
|
||||
|
||||
// Encode header and payload
|
||||
StringBuilder jwtBuilder = new StringBuilder();
|
||||
|
||||
// Header
|
||||
jwtBuilder.append(base64UrlEncode(mapToJson(header)));
|
||||
jwtBuilder.append(".");
|
||||
|
||||
// Payload
|
||||
jwtBuilder.append(base64UrlEncode(mapToJson(payload)));
|
||||
jwtBuilder.append(".");
|
||||
|
||||
// Signature (simplified - would use proper DID signing)
|
||||
String signature = createSignature(jwtBuilder.toString(), did);
|
||||
jwtBuilder.append(signature);
|
||||
|
||||
String jwt = jwtBuilder.toString();
|
||||
Log.d(TAG, "JWT signed successfully (length: " + jwt.length() + ")");
|
||||
|
||||
return jwt;
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error signing JWT", e);
|
||||
throw new RuntimeException("Failed to sign JWT", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JWT signature (simplified for Phase 1)
|
||||
*
|
||||
* @param data Data to sign
|
||||
* @param did DID for signature
|
||||
* @return Base64-encoded signature
|
||||
*/
|
||||
private String createSignature(String data, String did) throws Exception {
|
||||
// Phase 1: Simplified signature using DID hash
|
||||
// Production would use proper DID cryptographic signing
|
||||
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest((data + ":" + did).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return base64UrlEncode(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert map to JSON string (simplified)
|
||||
*/
|
||||
private String mapToJson(Map<String, Object> map) {
|
||||
StringBuilder json = new StringBuilder("{");
|
||||
boolean first = true;
|
||||
|
||||
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
||||
if (!first) json.append(",");
|
||||
json.append("\"").append(entry.getKey()).append("\":");
|
||||
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof String) {
|
||||
json.append("\"").append(value).append("\"");
|
||||
} else {
|
||||
json.append(value);
|
||||
}
|
||||
|
||||
first = false;
|
||||
}
|
||||
|
||||
json.append("}");
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 URL-safe encoding
|
||||
*/
|
||||
private String base64UrlEncode(byte[] data) {
|
||||
return Base64.getUrlEncoder()
|
||||
.withoutPadding()
|
||||
.encodeToString(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 URL-safe encoding for strings
|
||||
*/
|
||||
private String base64UrlEncode(String data) {
|
||||
return base64UrlEncode(data.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
// MARK: - Testing and Debugging
|
||||
|
||||
/**
|
||||
* Get current JWT token info for debugging
|
||||
*
|
||||
* @return Token information
|
||||
*/
|
||||
public String getTokenDebugInfo() {
|
||||
return String.format(
|
||||
"JWT Token Info - ActiveDID: %s, HasToken: %s, Expired: %s, ExpiresAt: %d",
|
||||
currentActiveDid,
|
||||
currentJWTToken != null,
|
||||
isTokenExpired(),
|
||||
jwtExpirationTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear authentication state
|
||||
*/
|
||||
public void clearAuthentication() {
|
||||
try {
|
||||
Log.d(TAG, "Clearing authentication state");
|
||||
|
||||
currentActiveDid = null;
|
||||
currentJWTToken = null;
|
||||
jwtExpirationTime = 0;
|
||||
|
||||
Log.i(TAG, "Authentication state cleared");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error clearing authentication", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user