Compare commits

...
2 Commits
Author SHA1 Message Date
Jeroen SchweitzerandClaude Opus 4.5 790ae41171 fix(auth): Riverpod lifecycle error + Authentik logout
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m2s
- Fix "Cannot use Ref after disposed" error in OIDC callback page
  - Store notifier reference before async gap
  - Add mounted check at start of processing
- Add proper SSO logout via Authentik end_session_endpoint
  - Clears local tokens AND redirects to Authentik logout
  - Returns to app after Authentik session ends

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 14:50:32 +01:00
Jeroen SchweitzerandClaude Opus 4.5 8625ac6574 fix(build): ensure fresh Flutter build on each deploy
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m1s
- Add flutter clean before build to prevent stale cached artifacts
- Add VERSION build arg for explicit cache busting
- Reorder build steps: clean → pub get → build_runner → health.json → build
- Replace deprecated dart:html with package:web in iframe_view_web.dart
- Add lint ignore to generate_health_json.dart

Fixes issue where Docker layer caching kept old main.dart.js
while regenerating health.json with new version number.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 14:18:01 +01:00
8 changed files with 107 additions and 19 deletions
+27
View File
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.1.1] - 2026-01-04
### Added
- Logout now redirects to Authentik to end SSO session
- Clears local tokens AND invalidates Authentik session
- Uses OIDC end_session_endpoint from discovery document
- Redirects back to app after Authentik logout completes
### Fixed
- Fixed Riverpod lifecycle error in OIDC callback page
- "Cannot use the Ref of authProvider after it has been disposed"
- Store notifier reference before async gap to prevent disposed ref access
- Add mounted check at start of callback processing
## [1.1.0] - 2026-01-04
### Changed
- **Dockerfile rebuild fix**: Added `flutter clean` before build to prevent stale cached artifacts
- VERSION build arg added for explicit cache busting
- Reordered build steps: clean → pub get → build_runner → health.json → flutter build
- Ensures deployed app always matches the version in health.json
### Fixed
- Replaced deprecated `dart:html` with `package:web` in iframe_view_web.dart
- Uses `web.HTMLIFrameElement` instead of `html.IFrameElement`
- Fixes deprecation warnings for Flutter 3.x web builds
## [1.0.12] - 2026-01-04
### Changed
+12 -4
View File
@@ -3,16 +3,24 @@ FROM ghcr.io/cirruslabs/flutter:stable AS builder
WORKDIR /app
# Copy dependency files first for better caching
COPY pubspec.yaml ./
# VERSION arg busts cache when version changes in pubspec.yaml
# Extract version: docker build --build-arg VERSION=$(grep '^version:' pubspec.yaml | cut -d' ' -f2) .
ARG VERSION=0.0.0
RUN echo "Building version: $VERSION"
# Get dependencies (generates pubspec.lock)
# Copy dependency files first for better caching
COPY pubspec.yaml pubspec.lock* ./
# Get dependencies
RUN flutter pub get
# Copy the rest of the application
COPY . .
# Generate code with build_runner
# Clean any cached build artifacts to ensure fresh build
RUN flutter clean && flutter pub get
# Generate code with build_runner (after clean for fresh generation)
RUN dart run build_runner build --delete-conflicting-outputs
# Generate health.json with version info
+18 -1
View File
@@ -341,10 +341,27 @@ class AuthNotifier extends _$AuthNotifier {
}
/// Sign out and clear stored credentials.
///
/// On web, also redirects to Authentik's logout endpoint to end the SSO session.
Future<void> signOut() async {
// Clear local storage first
await _clearStoredAuth();
state = const AsyncData(AuthState());
developer.log('Signed out', name: 'auth');
developer.log('Signed out locally', name: 'auth');
// On web, redirect to Authentik logout to end SSO session
if (kIsWeb && AppConfig.requiresAuth) {
try {
final oidcService = OidcServiceWeb();
final logoutUrl = await oidcService.getLogoutUrl();
developer.log('Redirecting to Authentik logout', name: 'auth');
web_utils.redirectTo(logoutUrl);
} catch (e) {
developer.log('Failed to get logout URL: $e', name: 'auth');
// Local logout already done, just reload to trigger re-auth
web_utils.redirectTo('/');
}
}
}
/// Update user preferences.
+27
View File
@@ -209,4 +209,31 @@ class OidcServiceWeb implements OidcService {
return List.generate(length, (_) => chars[random.nextInt(chars.length)])
.join();
}
/// Get the logout URL to redirect the browser to for SSO logout.
///
/// [idToken] is optional but recommended for logout verification.
/// After logout, Authentik redirects back to [postLogoutRedirectUri].
Future<String> getLogoutUrl({String? idToken}) async {
final discovery = await _fetchDiscovery();
final endSessionEndpoint = discovery['end_session_endpoint'] as String?;
if (endSessionEndpoint == null) {
// Fallback: just redirect to home, local state already cleared
developer.log('No end_session_endpoint in discovery', name: 'oidc_web');
return AppConfig.webBaseUrl;
}
final params = <String, String>{
'post_logout_redirect_uri': AppConfig.webBaseUrl,
};
if (idToken != null) {
params['id_token_hint'] = idToken;
}
final uri = Uri.parse(endSessionEndpoint).replace(queryParameters: params);
developer.log('Logout URL: $uri', name: 'oidc_web');
return uri.toString();
}
}
@@ -1,8 +1,8 @@
import 'dart:html' as html;
import 'dart:ui_web' as ui_web;
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:web/web.dart' as web;
/// Embedded iframe view for displaying external content (web only).
///
@@ -31,7 +31,7 @@ class IframeView extends StatefulWidget {
class _IframeViewState extends State<IframeView> {
late final String _viewType;
late html.IFrameElement _iframe;
late web.HTMLIFrameElement _iframe;
bool _isLoading = true;
@override
@@ -42,17 +42,18 @@ class _IframeViewState extends State<IframeView> {
}
void _createIframe() {
_iframe = html.IFrameElement()
_iframe = web.document.createElement('iframe') as web.HTMLIFrameElement
..src = widget.url
..style.border = 'none'
..style.width = '100%'
..style.height = '100%'
..allow = 'fullscreen'
..onLoad.listen((_) {
if (mounted) {
setState(() => _isLoading = false);
}
});
..allow = 'fullscreen';
_iframe.onLoad.listen((_) {
if (mounted) {
setState(() => _isLoading = false);
}
});
// Register the view factory
ui_web.platformViewRegistry.registerViewFactory(
+10 -4
View File
@@ -132,6 +132,9 @@ class _OidcCallbackPageState extends ConsumerState<_OidcCallbackPage> {
}
Future<void> _processCallback() async {
// Check mounted before any async work
if (!mounted) return;
// Check for error from Authentik
if (widget.error != null) {
setState(() {
@@ -150,12 +153,15 @@ class _OidcCallbackPageState extends ConsumerState<_OidcCallbackPage> {
return;
}
// Get notifier reference before async gap to avoid disposed ref errors
final authNotifier = ref.read(authProvider.notifier);
// Exchange code for tokens
try {
await ref.read(authProvider.notifier).handleOidcCallback(
widget.code!,
widget.callbackState!,
);
await authNotifier.handleOidcCallback(
widget.code!,
widget.callbackState!,
);
// Navigate to home on success
if (mounted) {
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.12+1
version: 1.1.1+1
environment:
sdk: ^3.10.4
+2
View File
@@ -2,6 +2,8 @@
// Generates web/health.json from pubspec.yaml
// Run: dart run tool/generate_health_json.dart
// ignore_for_file: avoid_print
import 'dart:convert';
import 'dart:io';