Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
37408eb
feat(AndroidManifest): add HTTP deep link for OAuth2 callbacks in emu…
maelemiel Oct 28, 2025
75403a6
feat(compatibility_rules): add endpoint to retrieve action-reaction c…
maelemiel Oct 28, 2025
4921a53
feat(about_service): add requires_oauth field to indicate OAuth authe…
maelemiel Oct 28, 2025
9852e01
feat: Add OAuth requirement to Service model and related components
maelemiel Oct 28, 2025
f144ad6
Merge branch 'main' of github.com:My-Epitech-Organisation/Area into h…
maelemiel Oct 28, 2025
ec68b7e
feat(flutter_svg): dependency to pubspec.yaml and update pubspec.lock
maelemiel Oct 29, 2025
6952ed1
feat(service): add logo field to Service model and update related com…
maelemiel Oct 29, 2025
c94def8
feat(compatibility_provider): update API endpoint to use base URL for…
maelemiel Oct 29, 2025
212b9a6
feat(AndroidManifest): add additional OAuth2 callback for port 8080
maelemiel Oct 29, 2025
2b6c431
fix(serializers): standardize quotes in service OAuth mapping
maelemiel Oct 29, 2025
789ba10
fix(service_card): improve error handling for service logo loading
maelemiel Oct 29, 2025
4598877
fix(service_connections): add error handling for service logo loading
maelemiel Oct 29, 2025
0af81b5
fix(compatibility_provider): handle uninitialized rules in reaction c…
maelemiel Oct 29, 2025
7735a2d
fix(create_applet_page): rename actionConfig to triggerActionConfig f…
maelemiel Oct 29, 2025
0219aaf
fix(service_action_selector_card): remove unused import and simplify …
maelemiel Nov 1, 2025
dc2e831
fix(auth_service): improve error handling and remove debug logging fo…
maelemiel Nov 1, 2025
70bd47b
fix(applet): enhance JSON parsing for ActionData and ReactionData, im…
maelemiel Nov 1, 2025
59700ba
fix(applet_service): simplify error handling in JSON parsing for appl…
maelemiel Nov 1, 2025
828e597
fix(auth_provider): update user email assignment to handle different …
maelemiel Nov 1, 2025
3844691
fix(applet_provider): enhance debug logging for applet enrichment pro…
maelemiel Nov 1, 2025
5af4f3d
fix(service_connections): map service name before checking connection…
maelemiel Nov 1, 2025
d30d054
fix(login_page): add debug logging for unmounted widget during Google…
maelemiel Nov 1, 2025
2df4d1a
fix(http_client_service): remove debug print statements from GET and …
maelemiel Nov 1, 2025
2c6ae97
fix(debug_helper): enhance debug output formatting and add additional…
maelemiel Nov 1, 2025
db9167e
fix(google_sign_in_button): replace image icon with SVG for improved …
maelemiel Nov 1, 2025
c5fa19e
refactor(service_provider_config): simplify service provider configur…
maelemiel Nov 1, 2025
ba4716d
fix(main): update debug print statements for environment validation
maelemiel Nov 1, 2025
c5b5a82
fix(service): remove unused import and simplify display name logic
maelemiel Nov 1, 2025
745c9ad
fix(api_config): enhance debug output with emojis for better readability
maelemiel Nov 1, 2025
7657ff7
Merge branch 'main' of github.com:My-Epitech-Organisation/Area into h…
maelemiel Nov 1, 2025
fce58da
fix(tasks): update Gmail actions to include specific triggers for bet…
maelemiel Nov 1, 2025
3a3d90d
$Merge branch 'main' of github.com:My-Epitech-Organisation/Area into …
maelemiel Nov 1, 2025
b13d262
fix(tasks): format Gmail action names for better readability
maelemiel Nov 1, 2025
2486e49
refactor(CompatibilityProvider): remove debug print statements for cl…
maelemiel Nov 1, 2025
df4889c
Merge branch 'main' of github.com:My-Epitech-Organisation/Area into h…
maelemiel Nov 2, 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
27 changes: 26 additions & 1 deletion backend/automations/serializers.py
Comment thread
Arkteus marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -448,11 +448,12 @@ class AboutServiceSerializer(serializers.ModelSerializer):

actions = AboutActionSerializer(many=True, read_only=True)
reactions = AboutReactionSerializer(many=True, read_only=True)
requires_oauth = serializers.SerializerMethodField()
logo = serializers.SerializerMethodField()

class Meta:
model = Service
fields = ["name", "actions", "reactions", "logo"]
fields = ["name", "requires_oauth", "actions", "reactions", "logo"]

def get_logo(self, obj):
"""Return the configured logo URL for the service or a fallback.
Expand Down Expand Up @@ -509,6 +510,30 @@ def _normalize_key(s: str) -> str:

return found

def get_requires_oauth(self, obj):
"""Check if this service requires OAuth authentication.

Services that require OAuth:
- Direct OAuth providers (google, github, slack, spotify, twitch)
- Services that map to OAuth tokens (gmail->google, google_calendar->google)
"""
from django.conf import settings

oauth_names = settings.OAUTH2_PROVIDERS.keys()

# Direct OAuth providers
if obj.name in oauth_names:
return True

# Services that map to OAuth tokens
service_oauth_map = {
"gmail": "google",
"google_calendar": "google",
}
Comment thread
maelemiel marked this conversation as resolved.

mapped_oauth = service_oauth_map.get(obj.name)
return mapped_oauth in oauth_names if mapped_oauth else False


# Serializers pour Execution (journaling)

Expand Down
9 changes: 8 additions & 1 deletion backend/automations/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,14 @@ def check_gmail_actions(self):

try:
# Get all active Areas with Gmail actions
gmail_areas = get_active_areas(service_name="gmail")
gmail_areas = get_active_areas(
[
"gmail_new_email",
"gmail_new_from_sender",
"gmail_new_with_label",
"gmail_new_with_subject",
]
)

if not gmail_areas:
logger.info("No active Gmail areas found")
Expand Down
5 changes: 5 additions & 0 deletions backend/automations/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,9 @@
views.DebugExecutionsView.as_view({"get": "list"}),
name="debug-executions",
),
path(
"api/compatibility-rules/",
views.CompatibilityRulesView.as_view({"get": "list"}),
name="compatibility-rules",
),
]
25 changes: 25 additions & 0 deletions backend/automations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,3 +688,28 @@ def list(self, request, area_id=None):
{"error": "Area not found or access denied"},
status=status.HTTP_404_NOT_FOUND,
)


# Compatibility Rules View
# ============================================================================


@extend_schema(
tags=["Compatibility"],
summary="Get action-reaction compatibility rules",
description="Returns the compatibility rules mapping actions to compatible reactions. Public endpoint.",
)
class CompatibilityRulesView(viewsets.ViewSet):
"""Returns compatibility rules for action-reaction combinations."""

permission_classes = [permissions.AllowAny]

def list(self, request):
"""
Get all compatibility rules.

GET /api/compatibility-rules/
"""
from .validators import COMPATIBILITY_RULES

return Response(COMPATIBILITY_RULES, status=status.HTTP_200_OK)
13 changes: 13 additions & 0 deletions mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@
<!-- Handles OAuth callbacks like: area://auth/oauth/google/callback -->
<data android:scheme="area" android:host="auth"/>
</intent-filter>

<!-- HTTP Deep Link for OAuth2 Callbacks (Android Emulator) -->
<!-- This intercepts http://localhost:8082/auth/oauth/... callbacks from Google -->
<!-- The emulator browser can't reach localhost:8082 directly, but Android App Links -->
<!-- can intercept the intent and convert it to the area:// scheme -->
<intent-filter android:autoVerify="false">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<!-- Intercept localhost:8082 OAuth callbacks -->
<data android:scheme="http" android:host="localhost" android:port="8082" android:pathPattern="/auth/oauth/.*"/>
<data android:scheme="http" android:host="localhost" android:port="8080" android:pathPattern="/auth/oauth/.*"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
Expand Down
47 changes: 39 additions & 8 deletions mobile/lib/config/api_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ class ApiConfig {
if (portString.isNotEmpty) {
_configuredPort = int.tryParse(portString);
if (_configuredPort != null) {
ApiConfig.debugPrint('Backend port configured: $_configuredPort');
ApiConfig.debugPrint('✅ Backend port configured: $_configuredPort');
} else {
ApiConfig.debugPrint('❌ Failed to parse BACKEND_PORT: "$portString"');
}
} else {
ApiConfig.debugPrint(
'⚠️ BACKEND_PORT is empty, using default: ${AppConfig.defaultPort}',
);
}
}

static void setPort(int port) {
_configuredPort = port;
ApiConfig.debugPrint('Backend port set to: $port');
ApiConfig.debugPrint('Backend port set to: $port');
}

static String? _overrideBaseUrl;
Expand All @@ -26,7 +32,7 @@ class ApiConfig {

static void setBaseUrl(String url) {
_overrideBaseUrl = url.trim();
ApiConfig.debugPrint('Base URL override set to: $_overrideBaseUrl');
ApiConfig.debugPrint('Base URL override set to: $_overrideBaseUrl');
}

static void forceAndroidLocalhost(bool enable) {
Expand All @@ -44,21 +50,37 @@ class ApiConfig {
if (kIsWeb) {
// Web always uses localhost
host = AppConfig.defaultHost;
ApiConfig.debugPrint('🌐 Detected: Web - using localhost');
} else if (Platform.isAndroid && !_forceAndroidLocalhost) {
// Android emulator needs special host IP
host = AppConfig.androidEmulatorHost;
ApiConfig.debugPrint('🌐 Detected: Android emulator - using 10.0.2.2');
} else {
// iOS, physical devices, or forced localhost
host = AppConfig.defaultHost;
ApiConfig.debugPrint(
'🌐 Detected: Physical device/iOS - using localhost',
);
}

return _autoDetectedBase = 'http://$host:$port';
final baseUrl = 'http://$host:$port';
ApiConfig.debugPrint('🌐 Base URL: $baseUrl');
return _autoDetectedBase = baseUrl;
}

static String get baseUrl => _overrideBaseUrl ?? _detectBaseUrl();

static String get authBaseUrl => '$baseUrl/${AppConfig.authPrefix}';
static String get apiBaseUrl => '$baseUrl/${AppConfig.apiPrefix}';
static String get authBaseUrl {
final url = '$baseUrl/${AppConfig.authPrefix}';
ApiConfig.debugPrint('🔐 Auth base URL: $url');
return url;
}

static String get apiBaseUrl {
final url = '$baseUrl/${AppConfig.apiPrefix}';
ApiConfig.debugPrint('📡 API base URL: $url');
return url;
}

static String get loginUrl => '$authBaseUrl/login/';
static String get registerUrl => '$authBaseUrl/register/';
Expand All @@ -78,9 +100,18 @@ class ApiConfig {
static String get statisticsUrl => '$authBaseUrl/statistics';
static String get userStatisticsUrl => '$apiBaseUrl/users/statistics/';

static String get googleLoginUrl => '$authBaseUrl/google-login/';
static String get googleLoginUrl {
final url = '$authBaseUrl/google-login/';
ApiConfig.debugPrint('🔑 Google Login URL: $url');
return url;
}

static String automationUrl(int id) {
final url = '$apiBaseUrl/areas/$id/';
ApiConfig.debugPrint('📋 Automation URL ($id): $url');
return url;
}

static String automationUrl(int id) => '$apiBaseUrl/areas/$id/';
static String automationDuplicateUrl(int id) =>
'$apiBaseUrl/areas/$id/duplicate/';
static String automationPauseUrl(int id) => '$apiBaseUrl/areas/$id/pause/';
Expand Down
53 changes: 8 additions & 45 deletions mobile/lib/config/service_provider_config.dart
Original file line number Diff line number Diff line change
@@ -1,30 +1,14 @@
import '../utils/service_token_mapper.dart';

/// Service Provider Configuration
///
/// Centralized configuration for all OAuth service providers (Google, GitHub, etc.)
/// Lightweight configuration for service-related utilities.
/// Most service metadata (logos, OAuth info) comes from about.json
class ServiceProviderConfig {
static const List<String> oauthServices = [
'github',
'gmail',
'google',
'slack',
'twitch',
];

static bool requiresOAuth(String serviceName) {
// Map gmail to google for OAuth purposes
final mappedName = mapServiceName(serviceName);
return oauthServices.contains(mappedName.toLowerCase());
}

/// Map service names for consistency (gmail -> google)
/// Map service names for consistency (gmail/google_calendar -> google)
/// This is used to normalize service names across the application
static String mapServiceName(String serviceName) {
switch (serviceName.toLowerCase()) {
case 'gmail':
return 'google';
default:
return serviceName;
}
return ServiceTokenMapper.resolveTokenService(serviceName);
}

/// Get user-friendly provider name
Expand All @@ -37,33 +21,12 @@ class ServiceProviderConfig {
return 'GitHub';
case 'slack':
return 'Slack';
case 'spotify':
return 'Spotify';
case 'twitch':
return 'Twitch';
default:
return provider;
}
}

/// Get provider icon URL from web (PNG images)
static String getIconUrl(String provider) {
final mappedProvider = mapServiceName(provider);
switch (mappedProvider.toLowerCase()) {
case 'github':
return 'https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Flogos-world.net%2Fwp-content%2Fuploads%2F2020%2F11%2FGitHub-Symbol.png&f=1&nofb=1&ipt=1e8fe0d0c31dac1d47abf59a23130ec2b31975a34721d1ea9284db059ba4a957';
case 'google':
return 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Google_2015_logo.svg/256px-Google_2015_logo.svg.png';
case 'slack':
return 'https://cdn-icons-png.flaticon.com/512/2111/2111615.png';
case 'twitch':
return 'https://cdn-icons-png.flaticon.com/512/5968/5968819.png';
case 'timer':
return 'https://cdn-icons-png.flaticon.com/512/109/109613.png';
case 'email':
return 'https://cdn-icons-png.flaticon.com/512/542/542638.png';
case 'webhook':
return 'https://cdn-icons-png.flaticon.com/512/149/149852.png';
default:
return 'https://cdn-icons-png.flaticon.com/512/1828/1828970.png';
}
}
}
42 changes: 35 additions & 7 deletions mobile/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'providers/providers.dart';
import 'providers/compatibility_provider.dart';
import 'pages/splash_page.dart';
import 'pages/login_page.dart';
import 'pages/reset_password_page.dart';
Expand All @@ -21,9 +22,9 @@ void main() async {

try {
AppConfig.validateEnvironment();
AppConfig.debugPrint('Environment validation passed');
debugPrint('Environment validation passed');
} catch (e) {
AppConfig.debugPrint('Environment validation failed: $e');
debugPrint('Environment validation failed: $e');
if (AppConfig.isProduction) {
rethrow;
}
Expand All @@ -49,14 +50,16 @@ class _MyAppState extends State<MyApp> {
bool _initialLinkHandled = false;
late OAuthDeepLinkHandler _oauthHandler;

Uri? _lastHandledUri;
DateTime? _lastHandledTime;

@override
void initState() {
super.initState();
_appLinks = AppLinks();
_oauthHandler = OAuthDeepLinkHandler();
_oauthHandler.onOAuthComplete = _handleOAuthComplete;
_initDeepLinkListener();
_initOAuthHandler();
}

@override
Expand All @@ -66,10 +69,6 @@ class _MyAppState extends State<MyApp> {
super.dispose();
}

Future<void> _initOAuthHandler() async {
await _oauthHandler.initialize();
}

void _handleOAuthComplete(String provider, bool success, String? message) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final context = navigatorKey.currentContext;
Expand Down Expand Up @@ -116,6 +115,22 @@ class _MyAppState extends State<MyApp> {
void _handleDeepLink(Uri uri) {
debugPrint('Handling deep link: $uri');

final now = DateTime.now();
if (_lastHandledUri?.toString() == uri.toString() &&
_lastHandledTime != null &&
now.difference(_lastHandledTime!).inSeconds < 2) {
debugPrint('Duplicate deep link ignored (same URI within 2 seconds)');
return;
}

_lastHandledUri = uri;
_lastHandledTime = now;

if (_oauthHandler.isOAuthCallback(uri)) {
_oauthHandler.handleDeepLink(uri);
return;
}

if (uri.scheme == AppConfig.urlScheme &&
uri.host == AppConfig.resetPasswordDeepLink) {
final token = uri.queryParameters['token'];
Expand Down Expand Up @@ -146,6 +161,7 @@ class _MyAppState extends State<MyApp> {
ChangeNotifierProvider(create: (_) => ServiceCatalogProvider()),
ChangeNotifierProvider(create: (_) => ConnectedServicesProvider()),
ChangeNotifierProvider(create: (_) => AutomationStatsProvider()),
ChangeNotifierProvider(create: (_) => CompatibilityProvider()),
],
child: Builder(
builder: (context) {
Expand All @@ -167,6 +183,18 @@ class _MyAppState extends State<MyApp> {
}
};

// Load compatibility rules when app starts
WidgetsBinding.instance.addPostFrameCallback((_) {
final compatibilityProvider = Provider.of<CompatibilityProvider>(
context,
listen: false,
);
if (!compatibilityProvider.isLoaded &&
!compatibilityProvider.isLoading) {
compatibilityProvider.loadRules();
}
});

return MaterialApp(
navigatorKey: navigatorKey,
title: '${AppConfig.appName} Mobile',
Expand Down
Loading
Loading