From b3de3eec8c5ac3cfe1ae800d6e8a2d9b1bc710ad Mon Sep 17 00:00:00 2001 From: verboomp Date: Wed, 21 Jan 2026 16:08:09 +0100 Subject: [PATCH] added frontend --- .gitignore | 30 +- Jenkinsfile | 4 +- .../core/service/CustomerPictureService.java | 2 +- .../core/utils/JwtTokenUtil.java | 4 +- .../core/utils/LoginUtils.java | 4 +- .../fotodocumentation/rest/LoginResource.java | 3 +- ...-foto-documentation-web-1.0.0-SNAPSHOT.war | Bin 26668304 -> 26668182 bytes .../docker/standalone-fotodocumentation.xml | 2 +- .../rest/CustomerResourceTest.java | 2 - .../.claude/settings.local.json | 12 + .../.gitignore | 45 + .../.metadata | 30 + .../.vscode/launch.json | 25 + .../README.md | 16 + .../analysis_options.yaml | 28 + .../assets/theme/appainter_theme.json | 1113 +++++++++++++++++ .../devtools_options.yaml | 3 + .../l10n.yaml | 3 + .../lib/controller/base_controller.dart | 174 +++ .../lib/controller/login_controller.dart | 102 ++ .../lib/dto/base_dto.dart | 15 + .../lib/dto/jwt_token_pair_dto.dart | 50 + .../lib/l10n/app_de.arb | 75 ++ .../lib/l10n/app_localizations.dart | 217 ++++ .../lib/l10n/app_localizations_de.dart | 60 + .../lib/main.dart | 49 + .../lib/pages/customer/customer_widget.dart | 15 + .../lib/pages/landing_page_widget.dart | 23 + .../lib/pages/login/login_widget.dart | 154 +++ .../component/general_error_widget.dart | 55 + .../component/general_submit_widget.dart | 32 + .../component/page_header_widget.dart | 66 + .../component/search_bar_card_widget.dart | 81 ++ .../ui_utils/component/text_input_widget.dart | 60 + .../ui_utils/component/waiting_widget.dart | 26 + .../pages/ui_utils/dialog/delete_dialog.dart | 173 +++ .../pages/ui_utils/dialog/dialog_result.dart | 11 + .../pages/ui_utils/dialog/snackbar_utils.dart | 85 ++ .../lib/pages/ui_utils/general_style.dart | 36 + .../pages/ui_utils/header_button_wrapper.dart | 89 ++ .../lib/pages/ui_utils/header_utils.dart | 75 ++ .../lib/pages/ui_utils/modern_app_bar.dart | 99 ++ .../lib/utils/date_time_utils.dart | 17 + .../lib/utils/di_container.dart | 38 + .../lib/utils/extensions.dart | 29 + .../lib/utils/global_router.dart | 51 + .../lib/utils/global_stack.dart | 16 + .../lib/utils/http_client_factory_app.dart | 11 + .../lib/utils/http_client_factory_stub.dart | 21 + .../lib/utils/http_client_factory_web.dart | 14 + .../lib/utils/http_client_interceptor.dart | 37 + .../lib/utils/http_client_utils.dart | 20 + .../lib/utils/jwt_token_storage.dart | 69 + .../lib/utils/login_credentials.dart | 29 + .../lib/utils/main_utils.dart | 33 + .../lib/utils/password_utils.dart | 12 + .../lib/utils/url_utils.dart | 15 + hartmann-foto-documentation-frontend/pom.xml | 28 + .../pubspec.yaml | 122 ++ .../test/testing/test_http_client_utils.dart | 12 + .../test/testing/test_utils.dart | 67 + .../test/testing/test_utils.mocks.dart | 542 ++++++++ .../test/utils/date_time_utils_test.dart | 101 ++ .../utils/http_client_interceptor_test.dart | 133 ++ .../test/utils/jwt_token_storage_test.dart | 182 +++ .../test/utils/url_utils_test.dart | 12 + .../test_runner.dart | 132 ++ .../web/favicon.png | Bin 0 -> 917 bytes .../web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes .../web/index.html | 38 + .../web/manifest.json | 35 + 74 files changed, 4938 insertions(+), 26 deletions(-) create mode 100644 hartmann-foto-documentation-frontend/.claude/settings.local.json create mode 100644 hartmann-foto-documentation-frontend/.gitignore create mode 100644 hartmann-foto-documentation-frontend/.metadata create mode 100644 hartmann-foto-documentation-frontend/.vscode/launch.json create mode 100644 hartmann-foto-documentation-frontend/README.md create mode 100644 hartmann-foto-documentation-frontend/analysis_options.yaml create mode 100644 hartmann-foto-documentation-frontend/assets/theme/appainter_theme.json create mode 100644 hartmann-foto-documentation-frontend/devtools_options.yaml create mode 100644 hartmann-foto-documentation-frontend/l10n.yaml create mode 100644 hartmann-foto-documentation-frontend/lib/controller/base_controller.dart create mode 100644 hartmann-foto-documentation-frontend/lib/controller/login_controller.dart create mode 100644 hartmann-foto-documentation-frontend/lib/dto/base_dto.dart create mode 100644 hartmann-foto-documentation-frontend/lib/dto/jwt_token_pair_dto.dart create mode 100644 hartmann-foto-documentation-frontend/lib/l10n/app_de.arb create mode 100644 hartmann-foto-documentation-frontend/lib/l10n/app_localizations.dart create mode 100644 hartmann-foto-documentation-frontend/lib/l10n/app_localizations_de.dart create mode 100644 hartmann-foto-documentation-frontend/lib/main.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/customer/customer_widget.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/landing_page_widget.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/login/login_widget.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/general_error_widget.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/general_submit_widget.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/page_header_widget.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/search_bar_card_widget.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/text_input_widget.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/waiting_widget.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/delete_dialog.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/dialog_result.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/snackbar_utils.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/general_style.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/header_button_wrapper.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/header_utils.dart create mode 100644 hartmann-foto-documentation-frontend/lib/pages/ui_utils/modern_app_bar.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/date_time_utils.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/di_container.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/extensions.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/global_router.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/global_stack.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/http_client_factory_app.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/http_client_factory_stub.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/http_client_factory_web.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/http_client_interceptor.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/http_client_utils.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/jwt_token_storage.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/login_credentials.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/main_utils.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/password_utils.dart create mode 100644 hartmann-foto-documentation-frontend/lib/utils/url_utils.dart create mode 100644 hartmann-foto-documentation-frontend/pom.xml create mode 100644 hartmann-foto-documentation-frontend/pubspec.yaml create mode 100644 hartmann-foto-documentation-frontend/test/testing/test_http_client_utils.dart create mode 100644 hartmann-foto-documentation-frontend/test/testing/test_utils.dart create mode 100644 hartmann-foto-documentation-frontend/test/testing/test_utils.mocks.dart create mode 100644 hartmann-foto-documentation-frontend/test/utils/date_time_utils_test.dart create mode 100644 hartmann-foto-documentation-frontend/test/utils/http_client_interceptor_test.dart create mode 100644 hartmann-foto-documentation-frontend/test/utils/jwt_token_storage_test.dart create mode 100644 hartmann-foto-documentation-frontend/test/utils/url_utils_test.dart create mode 100644 hartmann-foto-documentation-frontend/test_runner.dart create mode 100644 hartmann-foto-documentation-frontend/web/favicon.png create mode 100644 hartmann-foto-documentation-frontend/web/icons/Icon-192.png create mode 100644 hartmann-foto-documentation-frontend/web/icons/Icon-512.png create mode 100644 hartmann-foto-documentation-frontend/web/icons/Icon-maskable-192.png create mode 100644 hartmann-foto-documentation-frontend/web/icons/Icon-maskable-512.png create mode 100644 hartmann-foto-documentation-frontend/web/index.html create mode 100644 hartmann-foto-documentation-frontend/web/manifest.json diff --git a/.gitignore b/.gitignore index 4191783..38d1c78 100644 --- a/.gitignore +++ b/.gitignore @@ -42,9 +42,9 @@ target/ */.dart_tool */build -skillmatrix-frontend/build -skillmatrix-frontend/coverage/ -skillmatrix-frontend/pubspec.lock +hartmann-foto-documentation-frontend/build +hartmann-foto-documentation-frontend/coverage/ +hartmann-foto-documentation-frontend/pubspec.lock # Avoid committing generated Javascript files: *.dart.js @@ -60,16 +60,16 @@ skillmatrix-frontend/pubspec.lock -skillmatrix-docker/src/main/docker/skillmatrix-web-*.war -skillmatrix-web/src/main/webapp/.last_build_id -skillmatrix-web/src/main/webapp/assets/ -skillmatrix-web/src/main/webapp/canvaskit/ -skillmatrix-web/src/main/webapp/favicon.png -skillmatrix-web/src/main/webapp/flutter.js -skillmatrix-web/src/main/webapp/flutter_bootstrap.js -skillmatrix-web/src/main/webapp/flutter_service_worker.js -skillmatrix-web/src/main/webapp/icons/ -skillmatrix-web/src/main/webapp/index.html -skillmatrix-web/src/main/webapp/manifest.json -skillmatrix-web/src/main/webapp/version.json +hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-*.war +hartmann-foto-documentation-web/src/main/webapp/.last_build_id +hartmann-foto-documentation-web/src/main/webapp/assets/ +hartmann-foto-documentation-web/src/main/webapp/canvaskit/ +hartmann-foto-documentation-web/src/main/webapp/favicon.png +hartmann-foto-documentation-web/src/main/webapp/flutter.js +hartmann-foto-documentation-web/src/main/webapp/flutter_bootstrap.js +hartmann-foto-documentation-web/src/main/webapp/flutter_service_worker.js +hartmann-foto-documentation-web/src/main/webapp/icons/ +hartmann-foto-documentation-web/src/main/webapp/index.html +hartmann-foto-documentation-web/src/main/webapp/manifest.json +hartmann-foto-documentation-web/src/main/webapp/version.json diff --git a/Jenkinsfile b/Jenkinsfile index b7f963a..66019c1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -58,7 +58,7 @@ pipeline { ''' } } -/* + stage ('Build Frontend') { steps { echo "running Frontend build for branch ${env.BRANCH_NAME}" @@ -112,7 +112,7 @@ pipeline { } } } -*/ + stage ('Build') { steps { echo "running build for branch ${env.BRANCH_NAME}" diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/CustomerPictureService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/CustomerPictureService.java index 5b19906..e1140bc 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/CustomerPictureService.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/CustomerPictureService.java @@ -43,6 +43,6 @@ public class CustomerPictureService extends AbstractService { // FIXME: do query List customers = queryService.callNamedQueryList(Customer.FIND_ALL); customers.forEach(c -> c.getPictures().size()); - return customers.parallelStream().map(c -> CustomerListValue.builder(c)).toList(); + return customers.parallelStream().map(CustomerListValue::builder).toList(); } } diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/JwtTokenUtil.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/JwtTokenUtil.java index 9256f87..bbc5c22 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/JwtTokenUtil.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/JwtTokenUtil.java @@ -38,8 +38,8 @@ public class JwtTokenUtil { private static final long REFRESH_TOKEN_VALIDITY = 30 * 24 * 60 * 60 * 1000L; // 30 days private static final long TEMP_2FA_TOKEN_VALIDITY = 5 * 60 * 1000L; // 5 minutes - private static final String ISSUER = "skillmatrix-jwt-issuer"; - private static final String AUDIENCE = "skillmatrix-api"; + private static final String ISSUER = "foto-jwt-issuer"; + private static final String AUDIENCE = "foto-api"; private PrivateKey privateKey; private PublicKey publicKey; diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/LoginUtils.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/LoginUtils.java index bf73385..34efbef 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/LoginUtils.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/LoginUtils.java @@ -43,13 +43,13 @@ public class LoginUtils { private Optional authenticate(String username, String password) { try { - LOG.error("Login with username: " + username + " password: " + password); + LOG.debug("Login with username & password " + username); Principal principal = new NamePrincipal(username); PasswordGuessEvidence evidence = new PasswordGuessEvidence(password.toCharArray()); SecurityDomain sd = SecurityDomain.getCurrent(); SecurityIdentity identity = sd.authenticate(principal, evidence); - LOG.error("Login identity: " + identity); + LOG.debug("Login identity: " + identity); return Optional.ofNullable(identity); } catch (RealmUnavailableException | SecurityException e) { LOG.warn("Failed to authenticate user " + e.getMessage(), e); diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/LoginResource.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/LoginResource.java index e456f28..ac8b009 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/LoginResource.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/LoginResource.java @@ -22,6 +22,7 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import marketing.heyday.hartmann.fotodocumentation.core.service.LoginService; import marketing.heyday.hartmann.fotodocumentation.core.utils.LoginUtils; import marketing.heyday.hartmann.fotodocumentation.rest.vo.TokenPairValue; @@ -58,7 +59,7 @@ public class LoginResource { Optional identity = loginUtils.authenticate(httpServletRequest); if (identity.isEmpty()) { LOG.debug("identity empty login invalid"); - return Response.status(401).build(); + return Response.status(Status.UNAUTHORIZED).build(); } String username = identity.get().getPrincipal().getName(); diff --git a/hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-1.0.0-SNAPSHOT.war b/hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-1.0.0-SNAPSHOT.war index 267279ba237ba3001c62a9d108b67ef04e7361e1..9f17fc9d705d7566f312507301b19e2847ccae30 100644 GIT binary patch delta 53873 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 Ow-pHlUQ;?kAp94W`ZQAj delta 53975 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 - DiContainer.get(); + JwtTokenStorage get _jwtTokenStorage => DiContainer.get(); + HttpClientUtils get httpClientUtils => DiContainer.get(); + + Future
getAuthHeader() async { + final accessToken = await _jwtTokenStorage.getAccessToken(); + + if (accessToken != null && accessToken.isNotEmpty) { + // Use JWT Bearer token + return Header('Authorization', 'Bearer $accessToken'); + } else { + return const Header("Accept-Language", "en-US"); + } + } + + Exception getServerError(Response response) { + return Exception("Error receiving data from server"); + } + + Future> runGetListWithAuth(String uriStr, List Function(dynamic) convert) async { + http.Client client = httpClientUtils.client; + try { + Header cred = await getAuthHeader(); + Uri uri = Uri.parse(uriStr); + var response = await client.get(uri, headers: {cred.name: cred.value}); + if (response.statusCode == 200) { + String text = response.body; + var jsonArray = jsonDecode(text); + return convert(jsonArray); + } else { + throw ServerError(response.statusCode); // Exception("Failed to get server data ${response.statusCode}"); + } + } catch (e) { + logger.e("exception $e"); + rethrow; + } + } + + Future runGetWithAuth(String uriStr, T Function(dynamic) convert) async { + http.Client client = httpClientUtils.client; + try { + Header cred = await getAuthHeader(); + Uri uri = Uri.parse(uriStr); + var response = await client.get(uri, headers: {cred.name: cred.value}); + if (response.statusCode == 200) { + String text = response.body; + var jsonArray = jsonDecode(text); + return convert(jsonArray); + } else { + throw ServerError(response.statusCode); // Exception("Failed to get server data ${response.statusCode}"); + } + } catch (e) { + logger.e("exception $e"); + rethrow; + } + } + + Future runDeleteWithAuth(String uriStr) async { + http.Client client = httpClientUtils.client; + Header cred = await getAuthHeader(); + Uri uri = Uri.parse(uriStr); + var response = await client.delete(uri, headers: {cred.name: cred.value}); + return response.statusCode == 200; + } + + Future runPutWithAuth(String uriStr) async { + http.Client client = httpClientUtils.client; + Header cred = await getAuthHeader(); + Uri uri = Uri.parse(uriStr); + var response = await client.put(uri, headers: {cred.name: cred.value}); + return response.statusCode == 200; + } + + Future> runSaveNew(String uriStr, T dtoObj, Function(http.Response response, T dto) processReply) async { + http.Client client = httpClientUtils.client; + try { + Header cred = await getAuthHeader(); + String body = jsonEncode(dtoObj.toMap()); + + Uri uri = Uri.parse(uriStr); + var response = await client.post(uri, headers: {cred.name: cred.value, "Accept": "application/json", "Content-Type": "application/json"}, body: body); + return processReply(response, dtoObj); + } catch (e) { + logger.e("exception $e"); + } + return ServerReply(ServerState.error, dtoObj); + } + + Future> runSaveUpdate(String uriStr, T dtoObj, Function(http.Response response, T dto) processReply) async { + http.Client client = httpClientUtils.client; + try { + Header cred = await getAuthHeader(); + String body = jsonEncode(dtoObj.toMap()); + + Uri uri = Uri.parse(uriStr); + var response = await client.put(uri, headers: {cred.name: cred.value, "Accept": "application/json", "Content-Type": "application/json"}, body: body); + + return processReply(response, dtoObj); + } catch (e) { + logger.e("exception $e"); + } + return ServerReply(ServerState.error, dtoObj); + } + + ServerReply processServerResponse(Response response, T dto, T Function(Map json) fromJson) { + if (response.statusCode == 200) { + String text = response.body; + var json = jsonDecode(text); + var dto = fromJson(json); + return ServerReply(ServerState.ok, dto); + } else if (response.statusCode == 400 || response.statusCode == 409) { + String text = response.body; + try { + var json = jsonDecode(text); + var error = ErrorDto.fromJson(json); + + return ServerReply(ServerState.duplicate, dto, error: error); + } catch (e) { + return ServerReply(ServerState.error, dto, error: ErrorDto(response.statusCode, text)); + } + } else if (response.statusCode == 403) { + var error = ErrorDto(403, "Not allowed."); + return ServerReply(ServerState.error, dto, error: error); + } + return ServerReply(ServerState.error, dto, error: ErrorDto(response.statusCode, "Internal server error")); + } +} + +class Header { + final String name; + final String value; + + const Header(this.name, this.value); + + @override + String toString() => '$runtimeType: $name, $value'; +} + +class ServerReply { + ServerState state; + T entity; + ErrorDto? error; + + ServerReply(this.state, this.entity, {this.error}); + + @override + String toString() => '$runtimeType: $state, $entity, $error'; +} + +class ServerError { + int statusCode; + + ServerError(this.statusCode); +} + +enum ServerState { + ok, + duplicate, + error, + ; +} diff --git a/hartmann-foto-documentation-frontend/lib/controller/login_controller.dart b/hartmann-foto-documentation-frontend/lib/controller/login_controller.dart new file mode 100644 index 0000000..d7b1c7b --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/controller/login_controller.dart @@ -0,0 +1,102 @@ +import 'dart:convert' show base64, utf8; +import 'dart:convert' show jsonDecode, jsonEncode; + +import 'package:fotodocumentation/controller/base_controller.dart'; +import 'package:fotodocumentation/dto/jwt_token_pair_dto.dart'; +import 'package:fotodocumentation/main.dart' show logger; +import 'package:fotodocumentation/utils/di_container.dart'; +import 'package:fotodocumentation/utils/jwt_token_storage.dart'; +import 'package:http/http.dart' as http; + +typedef AuthenticateReply = ({JwtTokenPairDto? jwtTokenPairDto}); + +abstract interface class LoginController { + Future authenticate(String username, String password); + Future refreshAccessToken(); + Future isUsingJwtAuth(); +} + +class LoginControllerImpl extends BaseController implements LoginController { + final String path = "login"; + + JwtTokenStorage get _jwtTokenStorage => DiContainer.get(); + + @override + Future authenticate(String username, String password) async { + http.Client client = httpClientUtils.client; + try { + Header cred = _getLoginHeader(username, password); + String uriStr = '${uriUtils.getBaseUrl()}$path'; + Uri uri = Uri.parse(uriStr); + + var response = await client.get(uri, headers: {cred.name: cred.value}); + if (response.statusCode == 200) { + final Map data = Map.castFrom(jsonDecode(response.body)); + + final tokenPair = JwtTokenPairDto.fromJson(data); + + // Store tokens securely + await _jwtTokenStorage.saveTokens(tokenPair.accessToken, tokenPair.refreshToken); + + // Load user data using the new token + return (jwtTokenPairDto: tokenPair); + } else { + logger.e('Authentication failed: ${response.statusCode} ${response.body}'); + return (jwtTokenPairDto: null); + } + } catch (e) { + logger.e("Authentication error: $e"); + return (jwtTokenPairDto: null); + } + } + + @override + Future refreshAccessToken() async { + try { + final refreshToken = await _jwtTokenStorage.getRefreshToken(); + if (refreshToken == null) { + logger.i('No refresh token available'); + return false; + } + String uriStr = '${uriUtils.getBaseUrl()}$path/login/refresh'; + Uri uri = Uri.parse(uriStr); + + final response = await http.post( + uri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'refreshToken': refreshToken, + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + final newAccessToken = data['accessToken'] as String; + + // Update only the access token (keep same refresh token) + await _jwtTokenStorage.updateAccessToken(newAccessToken); + + logger.d('Access token refreshed successfully'); + return true; + } else { + logger.d('Token refresh failed: ${response.statusCode} ${response.body}'); + return false; + } + } catch (e) { + logger.e('Token refresh error: $e'); + return false; + } + } + + @override + Future isUsingJwtAuth() async { + return await _jwtTokenStorage.hasTokens(); + } + + Header _getLoginHeader(String username, String password) { + String combined = "$username:$password"; + final bytes = utf8.encode(combined); + String asBase64 = base64.encode(bytes); + return Header("Authorization", "Basic $asBase64"); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/dto/base_dto.dart b/hartmann-foto-documentation-frontend/lib/dto/base_dto.dart new file mode 100644 index 0000000..2a47fdb --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/dto/base_dto.dart @@ -0,0 +1,15 @@ +final class ErrorDto { + int error; + String message; + + ErrorDto(this.error, this.message); + + ErrorDto.fromJson(Map json) + : error = json['error'] as int, + message = json['message']; +} + +abstract interface class DtoMapAble { + Map toMap(); +} + diff --git a/hartmann-foto-documentation-frontend/lib/dto/jwt_token_pair_dto.dart b/hartmann-foto-documentation-frontend/lib/dto/jwt_token_pair_dto.dart new file mode 100644 index 0000000..80caba9 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/dto/jwt_token_pair_dto.dart @@ -0,0 +1,50 @@ +/// DTO representing a failes login attempt request for 2fa token. +class TokenRequiredDto { + final bool? tokenRequired; + final bool? tokenInValid; + + TokenRequiredDto({ + required this.tokenRequired, + required this.tokenInValid, + }); + + /// Create from JSON response + factory TokenRequiredDto.fromJson(Map json) { + return TokenRequiredDto( + tokenRequired: json['tokenRequired'] as bool, + tokenInValid: json['tokenInValid'] as bool, + ); + } +} + +/// DTO representing a pair of JWT tokens from the backend. +class JwtTokenPairDto { + final String accessToken; + final String refreshToken; + + JwtTokenPairDto({ + required this.accessToken, + required this.refreshToken, + }); + + /// Create from JSON response + factory JwtTokenPairDto.fromJson(Map json) { + return JwtTokenPairDto( + accessToken: json['accessToken'] as String, + refreshToken: json['refreshToken'] as String, + ); + } + + /// Convert to JSON (for serialization if needed) + Map toJson() { + return { + 'accessToken': accessToken, + 'refreshToken': refreshToken, + }; + } + + @override + String toString() { + return 'JwtTokenPairDto{accessToken: [REDACTED], refreshToken: [REDACTED]}'; + } +} diff --git a/hartmann-foto-documentation-frontend/lib/l10n/app_de.arb b/hartmann-foto-documentation-frontend/lib/l10n/app_de.arb new file mode 100644 index 0000000..18f6c9f --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/l10n/app_de.arb @@ -0,0 +1,75 @@ +{ + "@@locale": "de", + "searchTFHint": "Suchtext", + "@searchTFHint": { + "description": "Search hint TextField" + }, + "searchButtonLabel": "Suchen", + "@searchButtonLabel": { + "description": "Search button label" + }, + "loginUsernameTFLabel": "Benutzername", + "@loginUsernameTFLabel": { + "description": "Usernamt TextField Label" + }, + "loginPasswordTFLabel": "Passwort", + "@loginPasswordTFLabel": { + "description": "Password TextField Label" + }, + "loginLoginButtonLabel": "Anmelden", + "@loginLoginButtonLabel": { + "description": "Login Button Label" + }, + "errorWidgetStatusCode": "Statuscode {statusCode}", + "@errorWidgetStatusCode": { + "description": "Error message showing server status code", + "placeholders": { + "statusCode": { + "type": "int" + } + } + }, + + "errorWidget": "Fehler: {name}", + "@errorWidget": { + "description": "Error widget text", + "placeholders": { + "name": { + "type": "String", + "example": "Error text" + } + } + }, + "errorWidgetRetryButton": "Wiederholen", + "@errorWidgetRetryButton": { + "description": "Retry button text for error widget" + }, + "submitWidget": "Speichern", + "@submitWidget": { + "description": "Save Button text" + }, + "textInputWidgetValidatorText": "Bitte geben Sie einen Text ein", + "@textInputWidgetValidatorText": { + "description": "Awaiting result info text" + }, + "waitingWidget": "Warten auf Ergebnis …", + "@waitingWidget": { + "description": "Awaiting result info text" + }, + "deleteDialogTitle": "Löschen", + "@deleteDialogTitle": { + "description": "Delete dialog title" + }, + "deleteDialogText": "Sind Sie sicher, dass Sie diese Eintrag löschen möchten?", + "@deleteDialogText": { + "description": "Delete dialog text" + }, + "deleteDialogButtonCancel": "Nein", + "@deleteDialogButtonCancel": { + "description": "Cancel Button text" + }, + "deleteDialogButtonApprove": "Ja", + "@deleteDialogButtonApprove": { + "description": "Approve Button text" + } +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/lib/l10n/app_localizations.dart b/hartmann-foto-documentation-frontend/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..af6b044 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/l10n/app_localizations.dart @@ -0,0 +1,217 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_de.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [Locale('de')]; + + /// Search hint TextField + /// + /// In de, this message translates to: + /// **'Suchtext'** + String get searchTFHint; + + /// Search button label + /// + /// In de, this message translates to: + /// **'Suchen'** + String get searchButtonLabel; + + /// Usernamt TextField Label + /// + /// In de, this message translates to: + /// **'Benutzername'** + String get loginUsernameTFLabel; + + /// Password TextField Label + /// + /// In de, this message translates to: + /// **'Passwort'** + String get loginPasswordTFLabel; + + /// Login Button Label + /// + /// In de, this message translates to: + /// **'Anmelden'** + String get loginLoginButtonLabel; + + /// Error message showing server status code + /// + /// In de, this message translates to: + /// **'Statuscode {statusCode}'** + String errorWidgetStatusCode(int statusCode); + + /// Error widget text + /// + /// In de, this message translates to: + /// **'Fehler: {name}'** + String errorWidget(String name); + + /// Retry button text for error widget + /// + /// In de, this message translates to: + /// **'Wiederholen'** + String get errorWidgetRetryButton; + + /// Save Button text + /// + /// In de, this message translates to: + /// **'Speichern'** + String get submitWidget; + + /// Awaiting result info text + /// + /// In de, this message translates to: + /// **'Bitte geben Sie einen Text ein'** + String get textInputWidgetValidatorText; + + /// Awaiting result info text + /// + /// In de, this message translates to: + /// **'Warten auf Ergebnis …'** + String get waitingWidget; + + /// Delete dialog title + /// + /// In de, this message translates to: + /// **'Löschen'** + String get deleteDialogTitle; + + /// Delete dialog text + /// + /// In de, this message translates to: + /// **'Sind Sie sicher, dass Sie diese Eintrag löschen möchten?'** + String get deleteDialogText; + + /// Cancel Button text + /// + /// In de, this message translates to: + /// **'Nein'** + String get deleteDialogButtonCancel; + + /// Approve Button text + /// + /// In de, this message translates to: + /// **'Ja'** + String get deleteDialogButtonApprove; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['de'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'de': + return AppLocalizationsDe(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.'); +} diff --git a/hartmann-foto-documentation-frontend/lib/l10n/app_localizations_de.dart b/hartmann-foto-documentation-frontend/lib/l10n/app_localizations_de.dart new file mode 100644 index 0000000..3851fc8 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/l10n/app_localizations_de.dart @@ -0,0 +1,60 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for German (`de`). +class AppLocalizationsDe extends AppLocalizations { + AppLocalizationsDe([String locale = 'de']) : super(locale); + + @override + String get searchTFHint => 'Suchtext'; + + @override + String get searchButtonLabel => 'Suchen'; + + @override + String get loginUsernameTFLabel => 'Benutzername'; + + @override + String get loginPasswordTFLabel => 'Passwort'; + + @override + String get loginLoginButtonLabel => 'Anmelden'; + + @override + String errorWidgetStatusCode(int statusCode) { + return 'Statuscode $statusCode'; + } + + @override + String errorWidget(String name) { + return 'Fehler: $name'; + } + + @override + String get errorWidgetRetryButton => 'Wiederholen'; + + @override + String get submitWidget => 'Speichern'; + + @override + String get textInputWidgetValidatorText => 'Bitte geben Sie einen Text ein'; + + @override + String get waitingWidget => 'Warten auf Ergebnis …'; + + @override + String get deleteDialogTitle => 'Löschen'; + + @override + String get deleteDialogText => + 'Sind Sie sicher, dass Sie diese Eintrag löschen möchten?'; + + @override + String get deleteDialogButtonCancel => 'Nein'; + + @override + String get deleteDialogButtonApprove => 'Ja'; +} diff --git a/hartmann-foto-documentation-frontend/lib/main.dart b/hartmann-foto-documentation-frontend/lib/main.dart new file mode 100644 index 0000000..42f8d02 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/main.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:logger/web.dart' show DateTimeFormat, Logger, PrettyPrinter; +import 'package:fotodocumentation/controller/login_controller.dart'; +import 'package:fotodocumentation/l10n/app_localizations.dart'; +import 'package:fotodocumentation/utils/di_container.dart'; +import 'package:fotodocumentation/utils/main_utils.dart'; +import 'package:fotodocumentation/utils/global_router.dart'; + +var logger = Logger( + printer: PrettyPrinter(methodCount: 2, errorMethodCount: 8, colors: true, printEmojis: true, dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart), +); + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + DiContainer.instance.initState(); + + final theme = await ThemeLoader.loadTheme(); + + LoginController loginController = DiContainer.get(); + //await loginController.isLoggedIn(); + runApp(FotoDocumentationApp(theme: theme)); +} + +class FotoDocumentationApp extends StatelessWidget { + final ThemeData theme; + + const FotoDocumentationApp({super.key, required this.theme}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Hartmann Foto App', + localizationsDelegates: [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + Locale('de'), + ], + scrollBehavior: MyCustomScrollBehavior(), // <== needed for web horizontal scroll behavior + theme: theme, + routerConfig: GlobalRouter.router); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/customer/customer_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/customer/customer_widget.dart new file mode 100644 index 0000000..72f592d --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/customer/customer_widget.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class CustomerWidget extends StatefulWidget { + const CustomerWidget({super.key}); + + @override + State createState() => _CustomerWidgetState(); +} + +class _CustomerWidgetState extends State { + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/lib/pages/landing_page_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/landing_page_widget.dart new file mode 100644 index 0000000..a636bce --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/landing_page_widget.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class LandingPageWidget extends StatefulWidget { + final Widget child; + const LandingPageWidget({super.key, required this.child}); + + @override + State createState() => _LandingPageWidgetState(); +} + +class _LandingPageWidgetState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: widget.child, + ); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/login/login_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/login/login_widget.dart new file mode 100644 index 0000000..3bd35a3 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/login/login_widget.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:go_router/go_router.dart'; + +import 'package:fotodocumentation/controller/login_controller.dart'; +import 'package:fotodocumentation/dto/jwt_token_pair_dto.dart'; +import 'package:fotodocumentation/l10n/app_localizations.dart'; +import 'package:fotodocumentation/pages/ui_utils/component/general_submit_widget.dart'; +import 'package:fotodocumentation/pages/ui_utils/header_utils.dart'; +import 'package:fotodocumentation/pages/ui_utils/modern_app_bar.dart'; +import 'package:fotodocumentation/utils/di_container.dart'; +import 'package:fotodocumentation/utils/login_credentials.dart'; + +class LoginWidget extends StatefulWidget { + const LoginWidget({super.key}); + + @override + State createState() => _LoginWidgetState(); +} + +class _LoginWidgetState extends State { + HeaderUtils get _headerUtils => DiContainer.get(); + LoginController get _loginController => DiContainer.get(); + LoginCredentials get _loginCredentials => DiContainer.get(); + + final GlobalKey _formKey = GlobalKey(); + + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + + String? _error; + final FocusNode _focusNode = FocusNode(); + + @override + void dispose() { + _focusNode.dispose(); + _usernameController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ModernAppBar( + title: _headerUtils.titleWidget("Login title"), + actions: [], + ), + body: _body(context), + ); + } + + Widget _body(BuildContext context) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.grey[50]!, + Colors.white, + ], + ), + ), + child: _content(context), + ); + } + + Widget _content(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(20.0), + child: Form( + key: _formKey, + child: KeyboardListener( + focusNode: _focusNode, + onKeyEvent: (event) { + if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) { + _actionSubmit(context); + } + }, + child: ListView( + children: [ + Card( + elevation: 4, + margin: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + child: Padding( + padding: const EdgeInsets.all(30.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + key: Key("username"), + controller: _usernameController, + decoration: InputDecoration( + border: UnderlineInputBorder(), + labelText: AppLocalizations.of(context)!.loginUsernameTFLabel, + ), + ), + const SizedBox(height: 10), + TextFormField( + key: Key("password"), + controller: _passwordController, + obscureText: true, + decoration: InputDecoration( + border: UnderlineInputBorder(), + labelText: AppLocalizations.of(context)!.loginPasswordTFLabel, + ), + ), + const SizedBox(height: 10), + if (_error != null) ...[ + Text( + _error!, + style: const TextStyle(color: Colors.red), + ), + const SizedBox(height: 10), + ], + GeneralSubmitWidget( + key: const Key("submit"), + onSelect: () async => await _actionSubmit(context), + title: AppLocalizations.of(context)!.loginLoginButtonLabel, + ), + const SizedBox(height: 30), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + Future _actionSubmit(BuildContext context) async { + String username = _usernameController.text; + String password = _passwordController.text; + + AuthenticateReply authenticateReply = await _loginController.authenticate(username, password); + + JwtTokenPairDto? jwtTokenPairDto = authenticateReply.jwtTokenPairDto; + if (jwtTokenPairDto == null) { + setState(() => _error = "Error message"); + return; + } + + _loginCredentials.setLoggedIn(true); + if (context.mounted) { + context.go("/"); + } + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/general_error_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/general_error_widget.dart new file mode 100644 index 0000000..3dfbc98 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/general_error_widget.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:fotodocumentation/l10n/app_localizations.dart'; +import 'package:fotodocumentation/controller/base_controller.dart'; + +class GeneralErrorWidget extends StatelessWidget { + final String error; + final Function()? reload; + final int? statusCode; + + const GeneralErrorWidget({super.key, required this.error, this.reload, this.statusCode}); + + factory GeneralErrorWidget.fromServerError(ServerError serverError, {Function()? reload}) { + return GeneralErrorWidget(error: "", reload: reload, statusCode: serverError.statusCode); + } + + factory GeneralErrorWidget.fromSnapshot(AsyncSnapshot snapshot, {Function()? reload}) { + var error = snapshot.error; + if (error is ServerError) { + return GeneralErrorWidget.fromServerError(error, reload: () => reload); + } + return GeneralErrorWidget(error: snapshot.error.toString(), reload: () => reload); + } + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + final String errorMessage = statusCode != null + ? localizations.errorWidgetStatusCode(statusCode!) + : localizations.errorWidget(error); + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 60, color: Colors.red[300]), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + errorMessage, + style: TextStyle(color: Colors.red[700]), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 16), + if (reload != null) ...[ + ElevatedButton( + onPressed: reload, + child: Text(localizations.errorWidgetRetryButton), + ), + ], + ], + ), + ); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/general_submit_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/general_submit_widget.dart new file mode 100644 index 0000000..5d3b993 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/general_submit_widget.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:fotodocumentation/l10n/app_localizations.dart'; +import 'package:fotodocumentation/pages/ui_utils/general_style.dart'; +import 'package:fotodocumentation/utils/di_container.dart'; + +typedef SubmitCallback = void Function(); + +class GeneralSubmitWidget extends StatelessWidget { + GeneralStyle get _generalStyle => DiContainer.get(); + + final SubmitCallback onSelect; + final String? title; + + const GeneralSubmitWidget({super.key, required this.onSelect, this.title}); + + @override + Widget build(BuildContext context) { + String text = title ?? AppLocalizations.of(context)!.submitWidget; + return Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ElevatedButton( + key: Key("SubmitWidgetButton"), + style: _generalStyle.elevatedButtonStyle, + onPressed: onSelect, + child: Text(text), + ), + ], + ); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/page_header_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/page_header_widget.dart new file mode 100644 index 0000000..2203e39 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/page_header_widget.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +class PageHeaderWidget extends StatelessWidget { + final IconData iconData; + final String text; + final String subText; + final Color? iconColor; + + const PageHeaderWidget({super.key, this.iconData = Icons.business, required this.text, this.subText = "", this.iconColor}); + + @override + Widget build(BuildContext context) { + final color = iconColor ?? Theme.of(context).colorScheme.primary; + return Card( + elevation: 2, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Colors.grey[300]!, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withAlpha(51), + shape: BoxShape.circle, + ), + child: Icon( + iconData, + size: 32, + color: color, + ), + ), + const SizedBox(width: 16), + Text( + text, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + if (subText.isNotEmpty) ...[ + const SizedBox(height: 16), + Text( + subText, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey[600], + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/search_bar_card_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/search_bar_card_widget.dart new file mode 100644 index 0000000..2f4c052 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/search_bar_card_widget.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:fotodocumentation/l10n/app_localizations.dart'; + +class SearchBarCardWidget extends StatefulWidget { + final TextEditingController searchController; + final Function(String) onSearch; + const SearchBarCardWidget({super.key, required this.searchController, required this.onSearch}); + + @override + State createState() => _SearchBarCardWidgetState(); +} + +class _SearchBarCardWidgetState extends State { + @override + Widget build(BuildContext context) { + return Card( + elevation: 2, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Colors.grey[300]!, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextField( + key: Key("Search_text_field"), + controller: widget.searchController, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + hintText: AppLocalizations.of(context)!.searchTFHint, + border: InputBorder.none, + prefixIcon: const Icon(Icons.search, size: 28), + contentPadding: EdgeInsets.zero, + isDense: true, + suffixIcon: InkWell( + key: Key("Search_text_clear_button"), + onTap: () => _actionClear(), + child: const Icon( + Icons.close, + color: Colors.black, + ), + ) + ), + onSubmitted: (_) => _actionSubmit(), + ), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + key: Key("Search_text_button"), + onPressed: _actionSubmit, + icon: const Icon(Icons.search, size: 18), + label: Text(AppLocalizations.of(context)!.searchButtonLabel), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ), + ); + } + + void _actionSubmit() { + widget.onSearch(widget.searchController.text); + } + + void _actionClear() { + widget.searchController.text = ""; + widget.onSearch(widget.searchController.text); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/text_input_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/text_input_widget.dart new file mode 100644 index 0000000..e09551f --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/text_input_widget.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:fotodocumentation/l10n/app_localizations.dart'; + +import 'package:provider/provider.dart'; + +class TextInputWidget extends StatelessWidget { + final String labelText; + final bool required; + final bool obscureText; + final bool readOnly; + final Function? onTap; + const TextInputWidget({super.key, required this.labelText, this.required = false, this.obscureText = false, this.readOnly = false, this.onTap}); + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, controller, child) { + return TextFormField( + readOnly: readOnly, + obscureText: obscureText, + controller: controller, + decoration: InputDecoration( + border: const UnderlineInputBorder(), + labelText: labelText, + ), + validator: (String? value) => required && (value == null || value.isEmpty) ? AppLocalizations.of(context)!.textInputWidgetValidatorText : null, + onTap: () => onTap?.call(), + ); + }); + } +} + +class TextMultiInputWidget extends StatelessWidget { + final String labelText; + final bool required; + final bool obscureText; + final bool readOnly; + final Function? onTap; + final int maxLines; + const TextMultiInputWidget({super.key, required this.labelText, this.required = false, this.obscureText = false, this.readOnly = false, this.maxLines = 6, this.onTap}); + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, controller, child) { + return TextFormField( + readOnly: readOnly, + minLines: 3, // Set this + maxLines: maxLines, // and this + keyboardType: TextInputType.multiline, + obscureText: obscureText, + controller: controller, + decoration: InputDecoration( + border: const UnderlineInputBorder(), + labelText: labelText, + ), + validator: (String? value) => required && (value == null || value.isEmpty) ? AppLocalizations.of(context)!.textInputWidgetValidatorText : null, + onTap: () => onTap?.call(), + ); + }); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/waiting_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/waiting_widget.dart new file mode 100644 index 0000000..3d6a3d6 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/waiting_widget.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:fotodocumentation/l10n/app_localizations.dart'; + +class WaitingWidget extends StatelessWidget { + const WaitingWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + width: 60, + height: 60, + child: CircularProgressIndicator(), + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text(AppLocalizations.of(context)!.waitingWidget), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/delete_dialog.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/delete_dialog.dart new file mode 100644 index 0000000..471df04 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/delete_dialog.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; + +import 'package:fotodocumentation/main.dart' show logger; +import 'package:fotodocumentation/l10n/app_localizations.dart'; +import 'package:fotodocumentation/pages/ui_utils/dialog/snackbar_utils.dart'; +import 'package:fotodocumentation/utils/di_container.dart'; + +class DeleteDialog extends StatelessWidget { + static SnackbarUtils get _snackbarUtils => DiContainer.get(); + + const DeleteDialog({super.key}); + + static Future show(BuildContext context, Future Function() doDelete) async { + await _openDialog(context).then((value) async { + if (value != null && value && context.mounted) { + logger.d("Delete popup result $value"); + var result = await doDelete(); + if (context.mounted && result.msg.isNotEmpty) { + _snackbarUtils.showSnackbar(context, result.msg, result.warning); + } + } + }); + } + + static Future _openDialog(BuildContext context) async { + return showDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return const DeleteDialog( + key: Key("delete_dialog"), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; + + return AlertDialog( + backgroundColor: Colors.white, + scrollable: false, + titlePadding: const EdgeInsets.all(16.0), + contentPadding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 16.0), + title: _titleWidget(context, loc), + content: _content(context, loc), + ); + } + + Widget _titleWidget(BuildContext context, AppLocalizations loc) { + return Card( + elevation: 4, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Colors.grey[300]!, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.warning_amber_rounded, + size: 32, + color: Colors.orange[700], + ), + const SizedBox(width: 16), + Text( + loc.deleteDialogTitle, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + IconButton( + key: const Key("close_button"), + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context, false), + ), + ], + ), + ), + ); + } + + Widget _content(BuildContext context, AppLocalizations loc) { + return SizedBox( + width: 400, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + elevation: 2, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Colors.grey[300]!, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon( + Icons.warning_amber_rounded, + size: 48, + color: Colors.orange[700], + ), + const SizedBox(width: 16), + Expanded( + child: Text( + loc.deleteDialogText, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + key: const Key("delete_dialog:cancel"), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () => Navigator.pop(context, false), + child: Text(loc.deleteDialogButtonCancel), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + key: const Key("delete_dialog:approve"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + backgroundColor: Colors.red[600], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () => Navigator.pop(context, true), + icon: const Icon(Icons.delete, size: 20), + label: Text(loc.deleteDialogButtonApprove), + ), + ], + ), + ], + ), + ); + } +} + +class DeleteDialogResult { + final String msg; + final bool warning; + + DeleteDialogResult({required this.msg, required this.warning}); +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/dialog_result.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/dialog_result.dart new file mode 100644 index 0000000..682a170 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/dialog_result.dart @@ -0,0 +1,11 @@ +class DialogResult { + final DialogResultType type; + final T? dto; + + const DialogResult({required this.type, this.dto}); +} + +enum DialogResultType { + create, + add; +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/snackbar_utils.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/snackbar_utils.dart new file mode 100644 index 0000000..7712646 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/dialog/snackbar_utils.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +abstract interface class SnackbarUtils { + void showSnackbar(BuildContext context, String msg, bool warning); + void showSnackbarPopup(BuildContext context, String msg, bool warning); +} + +class SnackbarUtilsImpl implements SnackbarUtils { + @override + void showSnackbar(BuildContext context, String msg, bool warning) { + var snackBar = SnackBar( + content: _contentFor(context, msg, warning), + backgroundColor: Colors.white, + behavior: SnackBarBehavior.floating, + showCloseIcon: true, + closeIconColor: Theme.of(context).colorScheme.inversePrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + side: BorderSide(width: 2.0, style: BorderStyle.solid, color: Theme.of(context).colorScheme.inversePrimary), + ), + margin: EdgeInsets.only(bottom: MediaQuery.of(context).size.height - 130, left: MediaQuery.of(context).size.width - 400, right: 10), + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + @override + void showSnackbarPopup(BuildContext context, String msg, bool warning) { + var snackBar = SnackBar( + content: _contentFor(context, msg, warning), + backgroundColor: Colors.white, + behavior: SnackBarBehavior.floating, + showCloseIcon: true, + closeIconColor: Theme.of(context).colorScheme.inversePrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + side: BorderSide(width: 2.0, style: BorderStyle.solid, color: Theme.of(context).colorScheme.inversePrimary), + ), + width: 350, + //margin: EdgeInsets.only(bottom: MediaQuery.of(context).size.height - 100, left: MediaQuery.of(context).size.width - 350, right: 10), + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + Widget _contentFor(BuildContext context, String msg, bool warning) { + var icon = _iconFor(context, warning); + + var style = _textStyleFor(context, warning); + return Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + icon, + Text( + msg, + style: style, + ), + ], + ); + } + + Icon _iconFor(BuildContext context, bool warning) { + var color = _contentColor(context, warning); + return warning ? Icon(Icons.error, color: color) : Icon(Icons.check_circle_outline, color: color); + } + + TextStyle _textStyleFor(BuildContext context, bool warning) { + var color = _contentColor(context, warning); + var bodyLarge = Theme.of(context).primaryTextTheme.bodyLarge!; + var style = TextStyle( + color: color, + decoration: bodyLarge.decoration, + fontFamily: bodyLarge.fontFamily, + fontSize: bodyLarge.fontSize, + fontWeight: bodyLarge.fontWeight, + letterSpacing: bodyLarge.letterSpacing, + textBaseline: bodyLarge.textBaseline); + return style; + } + + Color _contentColor(BuildContext context, bool warning) { + return warning ? Theme.of(context).colorScheme.error : Theme.of(context).colorScheme.inversePrimary; + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/general_style.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/general_style.dart new file mode 100644 index 0000000..07bb923 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/general_style.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +import 'package:pinput/pinput.dart'; + +abstract interface class GeneralStyle { + PinTheme get pinTheme; + + ButtonStyle get elevatedButtonStyle; + ButtonStyle get roundedButtonStyle; +} + +class GeneralStyleImpl implements GeneralStyle { + static final ButtonStyle _elevatedButtonStyle = ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)); + static final ButtonStyle _roundedButtonStyle = ElevatedButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(8)); + + @override + PinTheme get pinTheme => _getPinTheme(); + + @override + ButtonStyle get elevatedButtonStyle => _elevatedButtonStyle; + + @override + ButtonStyle get roundedButtonStyle => _roundedButtonStyle; + + PinTheme _getPinTheme() { + return PinTheme( + width: 56, + height: 56, + textStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + ); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/header_button_wrapper.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/header_button_wrapper.dart new file mode 100644 index 0000000..7aedc2c --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/header_button_wrapper.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +class HeaderButtonWrapper extends StatefulWidget { + final IconData icon; + final String tooltip; + final VoidCallback onPressed; + final Color? iconColor; + final int? badgeCount; + + const HeaderButtonWrapper({ + super.key, + required this.icon, + required this.tooltip, + required this.onPressed, + this.iconColor, + this.badgeCount, + }); + + @override + State createState() => _HeaderButtonWrapperState(); +} + +class _HeaderButtonWrapperState extends State { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: Tooltip( + message: widget.tooltip, + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + decoration: BoxDecoration( + color: _isHovered ? Colors.blue[50] : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _isHovered ? Colors.blue[200]! : Colors.transparent, + width: 1, + ), + ), + child: IconButton( + icon: Icon( + widget.icon, + size: 24, + color: widget.iconColor ?? Colors.grey[700], + ), + onPressed: widget.onPressed, + splashRadius: 24, + ), + ), + if (widget.badgeCount != null && widget.badgeCount! > 0) + Positioned( + right: 4, + top: 4, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.red[600], + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + ), + constraints: const BoxConstraints( + minWidth: 20, + minHeight: 20, + ), + child: Text( + widget.badgeCount! > 99 ? '99+' : widget.badgeCount.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/header_utils.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/header_utils.dart new file mode 100644 index 0000000..3fc1fe8 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/header_utils.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:fotodocumentation/utils/di_container.dart'; +import 'package:fotodocumentation/utils/login_credentials.dart'; + +abstract interface class HeaderUtils { + Widget titleWidget(String text); +} + +class HeaderUtilsImpl extends HeaderUtils { + LoginCredentials get _loginCredentials => DiContainer.get(); + + @override + Widget titleWidget(String text) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.school, + size: 28, + color: Colors.blue[700], + ), + const SizedBox(width: 12), + Text( + text, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + const SizedBox(height: 4), + if (_loginCredentials.fullname.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.only(left: 40.0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.blue[200]!, + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.person_outline, + size: 14, + color: Colors.blue[700], + ), + const SizedBox(width: 6), + Text( + _loginCredentials.fullname, + style: TextStyle( + fontSize: 12, + color: Colors.blue[900], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ], + ); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/modern_app_bar.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/modern_app_bar.dart new file mode 100644 index 0000000..db7b262 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/modern_app_bar.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:fotodocumentation/pages/ui_utils/header_button_wrapper.dart'; + +class ModernAppBar extends StatelessWidget implements PreferredSizeWidget { + final Widget title; + final List actions; + final Widget? leading; + final bool automaticallyImplyLeading; + + const ModernAppBar({ + super.key, + required this.title, + this.actions = const [], + this.leading, + this.automaticallyImplyLeading = true, + }); + + @override + Widget build(BuildContext context) { + Widget? effectiveLeading = _effectiveLeading(context); + return Container( + decoration: BoxDecoration( + color: Colors.grey[50], + border: Border( + bottom: BorderSide( + color: Colors.grey[300]!, + width: 1, + ), + ), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + if (effectiveLeading != null) ...[ + effectiveLeading, + const SizedBox(width: 16), + ], + Expanded(child: title), + const SizedBox(width: 16), + ...actions.map((action) => Padding( + padding: const EdgeInsets.only(left: 8.0), + child: action, + )), + ], + ), + ), + ), + ); + } + + Widget? _effectiveLeading(BuildContext context) { + // Determine if we should show a back button + final ScaffoldState? scaffold = Scaffold.maybeOf(context); + final ModalRoute? parentRoute = ModalRoute.of(context); + final bool hasDrawer = scaffold?.hasDrawer ?? false; + final bool canPop = parentRoute?.canPop ?? false; + final bool useCloseButton = parentRoute is PageRoute && parentRoute.fullscreenDialog; + + Widget? effectiveLeading = leading; + if (effectiveLeading == null && automaticallyImplyLeading) { + if (hasDrawer) { + effectiveLeading = HeaderButtonWrapper( + icon: Icons.menu, + onPressed: () { + Scaffold.of(context).openDrawer(); + }, + tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, + iconColor: Colors.grey[700], + ); + } else if (canPop) { + if (useCloseButton) { + effectiveLeading = HeaderButtonWrapper( + icon: Icons.close, + onPressed: () { + Navigator.of(context).pop(); + }, + tooltip: MaterialLocalizations.of(context).closeButtonTooltip, + iconColor: Colors.grey[700], + ); + } else { + effectiveLeading = HeaderButtonWrapper( + icon: Icons.arrow_back, + onPressed: () { + Navigator.of(context).pop(); + }, + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + iconColor: Colors.grey[700], + ); + } + } + } + return effectiveLeading; + } + + @override + Size get preferredSize => const Size.fromHeight(80); +} diff --git a/hartmann-foto-documentation-frontend/lib/utils/date_time_utils.dart b/hartmann-foto-documentation-frontend/lib/utils/date_time_utils.dart new file mode 100644 index 0000000..9471d69 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/date_time_utils.dart @@ -0,0 +1,17 @@ +final class DateTimeUtils { + static DateTime? toDateTime(dynamic element) { + if (element == null) { + return null; + } + String text = element.toString(); + int? time = int.tryParse(text); + if (time == null) { + return null; + } + return DateTime.fromMillisecondsSinceEpoch(time); + } + + static int? fromDateTime(DateTime? dt) { + return dt?.millisecondsSinceEpoch; + } +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/lib/utils/di_container.dart b/hartmann-foto-documentation-frontend/lib/utils/di_container.dart new file mode 100644 index 0000000..8a43e06 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/di_container.dart @@ -0,0 +1,38 @@ +import 'package:fotodocumentation/controller/login_controller.dart'; +import 'package:fotodocumentation/pages/ui_utils/dialog/snackbar_utils.dart'; +import 'package:fotodocumentation/pages/ui_utils/general_style.dart'; +import 'package:fotodocumentation/pages/ui_utils/header_utils.dart'; +import 'package:fotodocumentation/utils/http_client_utils.dart'; +import 'package:fotodocumentation/utils/jwt_token_storage.dart'; +import 'package:fotodocumentation/utils/login_credentials.dart'; +import 'package:fotodocumentation/utils/url_utils.dart'; + +class DiContainer { + static final DiContainer instance = DiContainer._privateConstructor(); + DiContainer._privateConstructor(); + + final _container = {}; + + static T get() { + return instance._container[T] as T; + } + + void initState() { + DiContainer.instance.put(LoginCredentials, LoginCredentialsImpl()); + DiContainer.instance.put(GeneralStyle, GeneralStyleImpl()); + DiContainer.instance.put(JwtTokenStorage, JwtTokenStorageImpl()); + DiContainer.instance.put(HttpClientUtils, HttpCLientUtilsImpl()); + DiContainer.instance.put(HeaderUtils, HeaderUtilsImpl()); + DiContainer.instance.put(UrlUtils, UrlUtilsImpl()); + DiContainer.instance.put(SnackbarUtils, SnackbarUtilsImpl()); + DiContainer.instance.put(LoginController, LoginControllerImpl()); + } + + void put(Type key, T object) { + _container[key] = object; + } + + T get2() { + return _container[T] as T; + } +} diff --git a/hartmann-foto-documentation-frontend/lib/utils/extensions.dart b/hartmann-foto-documentation-frontend/lib/utils/extensions.dart new file mode 100644 index 0000000..9a99099 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/extensions.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart' show Colors, Color; + +extension HexColor on Color { + /// String is in the format "aabbcc" or "ffaabbcc" with an optional leading "#". + static Color fromHex(String hexString) { + final buffer = StringBuffer(); + if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); + buffer.write(hexString.replaceFirst('#', '')); + return Color(int.parse(buffer.toString(), radix: 16)); + } +} + +extension RiskColor on Color { + static const Color noRisk = Colors.transparent; + static final Color lowRisk = HexColor.fromHex("#FFFF00"); + static final Color mediumRisk = HexColor.fromHex("#FF9000"); + static final Color highRisk = HexColor.fromHex("#FF4000"); + + static Color colorForRisk(int value) { + if (value == 1) { + return lowRisk; + } else if (value == 2) { + return mediumRisk; + } else if (value == 3) { + return highRisk; + } + return noRisk; + } +} diff --git a/hartmann-foto-documentation-frontend/lib/utils/global_router.dart b/hartmann-foto-documentation-frontend/lib/utils/global_router.dart new file mode 100644 index 0000000..f92dc22 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/global_router.dart @@ -0,0 +1,51 @@ +// needed for web horizontal scroll behavior +import 'package:flutter/material.dart'; +import 'package:fotodocumentation/main.dart'; +import 'package:fotodocumentation/pages/customer/customer_widget.dart'; +import 'package:fotodocumentation/pages/login/login_widget.dart'; +import 'package:fotodocumentation/utils/di_container.dart'; +import 'package:fotodocumentation/utils/login_credentials.dart'; +import 'package:go_router/go_router.dart'; + +class GlobalRouter { + static final GlobalKey rootNavigatorKey = GlobalKey(debugLabel: 'root'); + static final GlobalKey bottomBarNavigatorKey = GlobalKey(debugLabel: 'bottombar'); + static final GlobalKey adminNavigatorKey = GlobalKey(debugLabel: 'admin'); + static final GlobalKey skillEditorNavigatorKey = GlobalKey(debugLabel: 'skillEditor'); + + static final String pathHome = "/home"; + static final String pathLogin = "/login"; + + static final GoRouter router = createRouter(pathHome); + + static GoRouter createRouter(String initialLocation) { + return GoRouter( + navigatorKey: rootNavigatorKey, + initialLocation: initialLocation, + routes: [ + GoRoute( + path: "/", + redirect: (_, __) => pathHome, + ), + GoRoute( + path: pathLogin, + builder: (BuildContext context, GoRouterState state) => const LoginWidget(), + ), + GoRoute( + path: pathHome, + builder: (context, state) => CustomerWidget(), + ), + ], + redirect: (context, state) { + var uriStr = state.uri.toString(); + logger.t("uri $uriStr"); + LoginCredentials loginCredentials = DiContainer.get(); + + if (!loginCredentials.isLoggedIn) { + return pathLogin; + } + return null; + }, + ); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/utils/global_stack.dart b/hartmann-foto-documentation-frontend/lib/utils/global_stack.dart new file mode 100644 index 0000000..a206d9e --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/global_stack.dart @@ -0,0 +1,16 @@ +class GlobalStack { + final _list = []; + + void push(T value) => _list.add(value); + + T pop() => _list.removeLast(); + + T peek() => _list.last; + + bool get isEmpty => _list.isEmpty; + + bool get isNotEmpty => _list.isNotEmpty; + + @override + String toString() => _list.toString(); +} diff --git a/hartmann-foto-documentation-frontend/lib/utils/http_client_factory_app.dart b/hartmann-foto-documentation-frontend/lib/utils/http_client_factory_app.dart new file mode 100644 index 0000000..30f4839 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/http_client_factory_app.dart @@ -0,0 +1,11 @@ +import 'package:http/http.dart' as http; +import 'package:fotodocumentation/utils/http_client_factory_stub.dart'; + +HttpCLientFactory getHttpClientFactory() => HttpClientFactoryApp(); + +class HttpClientFactoryApp extends HttpCLientFactory { + @override + http.Client createHttpClient() { + return http.Client(); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/utils/http_client_factory_stub.dart b/hartmann-foto-documentation-frontend/lib/utils/http_client_factory_stub.dart new file mode 100644 index 0000000..4ff8311 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/http_client_factory_stub.dart @@ -0,0 +1,21 @@ +import 'package:http/http.dart' as http; + +HttpCLientFactory getHttpClientFactory() => throw UnsupportedError('Cannot create http client'); + + +class HttpCLientFactory { + http.Client createHttpClient() { + // Check if running on the Web + /*if (kIsWeb) { + var client = http.Client(); + (client as BrowserClient).withCredentials = true; + return client; + } else if (universal_io.Platform.isAndroid || universal_io.Platform.isIOS) { + // Platform-specific logic for Android and iOS + return http.Client(); + } else { + throw UnsupportedError('Unsupported platform'); + }*/ + throw UnsupportedError('Cannot create http client'); + } +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/lib/utils/http_client_factory_web.dart b/hartmann-foto-documentation-frontend/lib/utils/http_client_factory_web.dart new file mode 100644 index 0000000..62b5883 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/http_client_factory_web.dart @@ -0,0 +1,14 @@ +import 'package:http/browser_client.dart'; +import 'package:http/http.dart' as http; +import 'package:fotodocumentation/utils/http_client_factory_stub.dart'; + +HttpCLientFactory getHttpClientFactory() => HttpClientFactoryWeb(); + +class HttpClientFactoryWeb extends HttpCLientFactory{ + @override + http.Client createHttpClient() { + var client = http.Client(); + (client as BrowserClient).withCredentials = true; + return client; + } +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/lib/utils/http_client_interceptor.dart b/hartmann-foto-documentation-frontend/lib/utils/http_client_interceptor.dart new file mode 100644 index 0000000..cec5ca8 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/http_client_interceptor.dart @@ -0,0 +1,37 @@ +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:fotodocumentation/utils/di_container.dart'; +import 'package:fotodocumentation/utils/login_credentials.dart'; +import 'package:fotodocumentation/utils/global_router.dart'; + +/// HTTP client that intercepts all responses and handles 401 status codes +/// by logging out the user and redirecting to the login page. +class HttpClientInterceptor extends http.BaseClient { + final http.Client _inner; + + HttpClientInterceptor(this._inner); + + @override + Future send(http.BaseRequest request) async { + final response = await _inner.send(request); + + // Check for 401 Unauthorized + if (response.statusCode == 401) { + _handle401Unauthorized(); + } + + return response; + } + + void _handle401Unauthorized() { + // Clear login credentials + final loginCredentials = DiContainer.get(); + loginCredentials.logout(); + + // Navigate to login page using GoRouter + final context = GlobalRouter.rootNavigatorKey.currentContext; + if (context != null) { + context.go(GlobalRouter.pathLogin); + } + } +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/lib/utils/http_client_utils.dart b/hartmann-foto-documentation-frontend/lib/utils/http_client_utils.dart new file mode 100644 index 0000000..cb44af9 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/http_client_utils.dart @@ -0,0 +1,20 @@ +import 'package:http/http.dart' as http; + +import 'http_client_factory_stub.dart' if (dart.library.io) 'http_client_factory_app.dart' if (dart.library.js) 'http_client_factory_web.dart'; +import 'http_client_interceptor.dart'; + +abstract class HttpClientUtils { + http.Client get client; +} + +class HttpCLientUtilsImpl extends HttpClientUtils { + http.Client? _client; + + @override + http.Client get client => _getClient(); + + http.Client _getClient() { + _client ??= HttpClientInterceptor(getHttpClientFactory().createHttpClient()); + return _client!; + } +} diff --git a/hartmann-foto-documentation-frontend/lib/utils/jwt_token_storage.dart b/hartmann-foto-documentation-frontend/lib/utils/jwt_token_storage.dart new file mode 100644 index 0000000..8b97210 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/jwt_token_storage.dart @@ -0,0 +1,69 @@ +abstract class JwtTokenStorage { + /// Save both access and refresh tokens + /// + /// @param accessToken The short-lived access token + /// @param refreshToken The long-lived refresh token + Future saveTokens(String accessToken, String refreshToken); + + /// Get the stored access token + /// + /// @return Access token or null if not found + Future getAccessToken(); + + /// Get the stored refresh token + /// + /// @return Refresh token or null if not found + Future getRefreshToken(); + + /// Clear all stored tokens (on logout) + Future clearTokens(); + + /// Check if tokens are stored + /// + /// @return true if access token exists + Future hasTokens(); + + /// Update only the access token (used after refresh) + /// + /// @param accessToken New access token + Future updateAccessToken(String accessToken); +} + +class JwtTokenStorageImpl extends JwtTokenStorage { + + // Storage keys + String? _keyAccessToken; + String? _keyRefreshToken; + + @override + Future saveTokens(String accessToken, String refreshToken) async { + _keyAccessToken = accessToken; + _keyRefreshToken = refreshToken; + } + + @override + Future getAccessToken() async { + return _keyAccessToken; + } + + @override + Future getRefreshToken() async { + return _keyRefreshToken; + } + + @override + Future clearTokens() async { + _keyAccessToken = null; + _keyRefreshToken = null; + } + + @override + Future hasTokens() async { + return _keyAccessToken != null && _keyAccessToken!.isNotEmpty; + } + + @override + Future updateAccessToken(String accessToken) async { + _keyAccessToken == accessToken; + } +} diff --git a/hartmann-foto-documentation-frontend/lib/utils/login_credentials.dart b/hartmann-foto-documentation-frontend/lib/utils/login_credentials.dart new file mode 100644 index 0000000..65e80f8 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/login_credentials.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +abstract class LoginCredentials extends ChangeNotifier { + String get fullname; + bool get isLoggedIn; + + void setLoggedIn(bool loggedIn); + void logout(); +} + +class LoginCredentialsImpl extends LoginCredentials { + bool loggedIn = false; + + @override + bool get isLoggedIn => loggedIn; + @override + String get fullname => ""; + + @override + void setLoggedIn(bool loggedIn) { + this.loggedIn = loggedIn; + } + + @override + void logout() { + loggedIn = false; + notifyListeners(); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/utils/main_utils.dart b/hartmann-foto-documentation-frontend/lib/utils/main_utils.dart new file mode 100644 index 0000000..ec60cab --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/main_utils.dart @@ -0,0 +1,33 @@ +// needed for web horizontal scroll behavior +import 'dart:convert' show jsonDecode; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:json_theme/json_theme.dart'; +import 'package:fotodocumentation/main.dart' show logger; + +class MyCustomScrollBehavior extends MaterialScrollBehavior { + // Override behavior methods and getters like dragDevices + @override + Set get dragDevices => { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }; +} + +class ThemeLoader { + static Future loadTheme() async { + try { + String prefix = kDebugMode && kIsWeb ? "" : "assets/"; + String url = "${prefix}theme/appainter_theme.json"; + final themeStr = await rootBundle.loadString(url); + final themeJson = jsonDecode(themeStr); + return ThemeDecoder.decodeThemeData(themeJson)!; + } catch (e) { + logger.e("Failed to load theme $e", error: e); + return ThemeData.light(); + } + } +} diff --git a/hartmann-foto-documentation-frontend/lib/utils/password_utils.dart b/hartmann-foto-documentation-frontend/lib/utils/password_utils.dart new file mode 100644 index 0000000..6ae9d61 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/password_utils.dart @@ -0,0 +1,12 @@ +import 'package:basic_utils/basic_utils.dart' show StringUtils; + +abstract interface class PasswordUtils { + String create(); +} + +class PasswordUtilsImpl implements PasswordUtils { + @override + String create() { + return StringUtils.generateRandomString(8, special: false); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/utils/url_utils.dart b/hartmann-foto-documentation-frontend/lib/utils/url_utils.dart new file mode 100644 index 0000000..c89d51f --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/utils/url_utils.dart @@ -0,0 +1,15 @@ +import 'package:flutter/foundation.dart' show kReleaseMode; + +abstract interface class UrlUtils { + String getBaseUrl(); +} + +class UrlUtilsImpl extends UrlUtils { + @override + String getBaseUrl() { + if (kReleaseMode){ + return "${Uri.base.origin}/api/"; + } + return "http://localhost:8080/api/"; + } +} diff --git a/hartmann-foto-documentation-frontend/pom.xml b/hartmann-foto-documentation-frontend/pom.xml new file mode 100644 index 0000000..d993e16 --- /dev/null +++ b/hartmann-foto-documentation-frontend/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + marketing.heyday.hartmann.fotodocumentation + hartmann-foto-documentation + 1.0.1 + ../hartmann-foto-documentation/pom.xml + + marketing.heyday.hartmann.fotodocumentation + hartmann-foto-documentation-frontend + 1.0.0-SNAPSHOT + pom + fotodocumentation-frontend + + + + + + + true + + + lib,pubspec.yaml + test + + true + + diff --git a/hartmann-foto-documentation-frontend/pubspec.yaml b/hartmann-foto-documentation-frontend/pubspec.yaml new file mode 100644 index 0000000..7e8743c --- /dev/null +++ b/hartmann-foto-documentation-frontend/pubspec.yaml @@ -0,0 +1,122 @@ +name: fotodocumentation +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# 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.0+1 + +environment: + sdk: '>=3.3.0 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.6 + http: ^1.2.2 + logger: ^2.4.0 + provider: ^6.1.2 + json_theme: ^9.0.1+2 + flutter_localizations: + sdk: flutter + intl: any + universal_io: ^2.2.2 + basic_utils: ^5.7.0 + file_picker: ^10.3.3 + http_parser: ^4.1.2 + go_router: ^16.2.4 + flutter_multi_select_items: ^0.4.3 + fl_chart: ^0.69.0 + pinput: ^6.0.1 + +# TODO: check if we can remove this in future contains a dependency override for Ticket +# https://github.com/peiffer-innovations/json_theme/issues/282 +# https://github.com/peiffer-innovations/json_theme/issues/287 +# https://github.com/peiffer-innovations/json_theme/pull/288 + +dependency_overrides: + + json_theme: + git: + url: https://github.com/wardbj93/json_theme + path: packages/json_theme + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + mockito: ^5.4.5 + build_runner: ^2.4.14 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +flutter_intl: + enabled: true + +# The following section is specific to Flutter packages. +flutter: + generate: true # Add this line for localization + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/theme/appainter_theme.json + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/hartmann-foto-documentation-frontend/test/testing/test_http_client_utils.dart b/hartmann-foto-documentation-frontend/test/testing/test_http_client_utils.dart new file mode 100644 index 0000000..cfb495e --- /dev/null +++ b/hartmann-foto-documentation-frontend/test/testing/test_http_client_utils.dart @@ -0,0 +1,12 @@ +import 'package:http/http.dart' as http; + +import 'package:fotodocumentation/utils/http_client_utils.dart'; + +class TestHttpCLientUtilsImpl extends HttpClientUtils { + http.Client testClient; + + TestHttpCLientUtilsImpl(this.testClient); + + @override + http.Client get client => testClient; +} diff --git a/hartmann-foto-documentation-frontend/test/testing/test_utils.dart b/hartmann-foto-documentation-frontend/test/testing/test_utils.dart new file mode 100644 index 0000000..d9ed5fa --- /dev/null +++ b/hartmann-foto-documentation-frontend/test/testing/test_utils.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:mockito/annotations.dart'; +import 'package:fotodocumentation/l10n/app_localizations.dart'; +import 'package:fotodocumentation/pages/ui_utils/dialog/snackbar_utils.dart'; +import 'package:fotodocumentation/pages/ui_utils/header_utils.dart'; +import 'package:fotodocumentation/utils/login_credentials.dart'; +import 'package:fotodocumentation/utils/password_utils.dart'; +import 'package:fotodocumentation/utils/jwt_token_storage.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:fotodocumentation/utils/global_router.dart'; + +import 'test_utils.mocks.dart'; + +void setScreenSize(WidgetTester tester, int width, int height) { + final dpi = tester.view.devicePixelRatio; + tester.view.physicalSize = Size(width * dpi, height * dpi); +} + +MockLoginCredentials getDefaultLoginCredentials() { + var mockLoginCredentials = MockLoginCredentials(); + return mockLoginCredentials; +} + +Future pumpApp(WidgetTester tester, Widget widget) async { + await tester.pumpWidget(MaterialApp( + title: 'App', + localizationsDelegates: [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + Locale('de'), + ], + home: Scaffold(body: widget))); + await tester.pump(); +} + +Future pumpAppConfig(WidgetTester tester, String initialLocation) async { + await tester.pumpWidget(MaterialApp.router( + title: 'App', + localizationsDelegates: [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + Locale('de'), + ], + routerConfig: GlobalRouter.createRouter(initialLocation))); + await tester.pump(); +} + +// dart run build_runner build +@GenerateMocks([ + LoginCredentials, + HeaderUtils, + PasswordUtils, + SnackbarUtils, + JwtTokenStorage, + http.Client +]) +void main() {} diff --git a/hartmann-foto-documentation-frontend/test/testing/test_utils.mocks.dart b/hartmann-foto-documentation-frontend/test/testing/test_utils.mocks.dart new file mode 100644 index 0000000..c5115ed --- /dev/null +++ b/hartmann-foto-documentation-frontend/test/testing/test_utils.mocks.dart @@ -0,0 +1,542 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in fotodocumentation/test/testing/test_utils.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i11; +import 'dart:convert' as _i12; +import 'dart:typed_data' as _i13; +import 'dart:ui' as _i6; + +import 'package:flutter/material.dart' as _i2; +import 'package:fotodocumentation/pages/ui_utils/dialog/snackbar_utils.dart' + as _i9; +import 'package:fotodocumentation/pages/ui_utils/header_utils.dart' as _i7; +import 'package:fotodocumentation/utils/jwt_token_storage.dart' as _i10; +import 'package:fotodocumentation/utils/login_credentials.dart' as _i4; +import 'package:fotodocumentation/utils/password_utils.dart' as _i8; +import 'package:http/http.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i5; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +class _FakeWidget_0 extends _i1.SmartFake implements _i2.Widget { + _FakeWidget_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i2.DiagnosticLevel? minLevel = _i2.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeResponse_1 extends _i1.SmartFake implements _i3.Response { + _FakeResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeStreamedResponse_2 extends _i1.SmartFake + implements _i3.StreamedResponse { + _FakeStreamedResponse_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [LoginCredentials]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLoginCredentials extends _i1.Mock implements _i4.LoginCredentials { + MockLoginCredentials() { + _i1.throwOnMissingStub(this); + } + + @override + String get fullname => (super.noSuchMethod( + Invocation.getter(#fullname), + returnValue: _i5.dummyValue( + this, + Invocation.getter(#fullname), + ), + ) as String); + + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + ) as bool); + + @override + void logout() => super.noSuchMethod( + Invocation.method( + #logout, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void addListener(_i6.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i6.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [HeaderUtils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHeaderUtils extends _i1.Mock implements _i7.HeaderUtils { + MockHeaderUtils() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Widget titleWidget(String? text) => (super.noSuchMethod( + Invocation.method( + #titleWidget, + [text], + ), + returnValue: _FakeWidget_0( + this, + Invocation.method( + #titleWidget, + [text], + ), + ), + ) as _i2.Widget); +} + +/// A class which mocks [PasswordUtils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPasswordUtils extends _i1.Mock implements _i8.PasswordUtils { + MockPasswordUtils() { + _i1.throwOnMissingStub(this); + } + + @override + String create() => (super.noSuchMethod( + Invocation.method( + #create, + [], + ), + returnValue: _i5.dummyValue( + this, + Invocation.method( + #create, + [], + ), + ), + ) as String); +} + +/// A class which mocks [SnackbarUtils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSnackbarUtils extends _i1.Mock implements _i9.SnackbarUtils { + MockSnackbarUtils() { + _i1.throwOnMissingStub(this); + } + + @override + void showSnackbar( + _i2.BuildContext? context, + String? msg, + bool? warning, + ) => + super.noSuchMethod( + Invocation.method( + #showSnackbar, + [ + context, + msg, + warning, + ], + ), + returnValueForMissingStub: null, + ); + + @override + void showSnackbarPopup( + _i2.BuildContext? context, + String? msg, + bool? warning, + ) => + super.noSuchMethod( + Invocation.method( + #showSnackbarPopup, + [ + context, + msg, + warning, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [JwtTokenStorage]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockJwtTokenStorage extends _i1.Mock implements _i10.JwtTokenStorage { + MockJwtTokenStorage() { + _i1.throwOnMissingStub(this); + } + + @override + _i11.Future saveTokens( + String? accessToken, + String? refreshToken, + ) => + (super.noSuchMethod( + Invocation.method( + #saveTokens, + [ + accessToken, + refreshToken, + ], + ), + returnValue: _i11.Future.value(), + returnValueForMissingStub: _i11.Future.value(), + ) as _i11.Future); + + @override + _i11.Future getAccessToken() => (super.noSuchMethod( + Invocation.method( + #getAccessToken, + [], + ), + returnValue: _i11.Future.value(), + ) as _i11.Future); + + @override + _i11.Future getRefreshToken() => (super.noSuchMethod( + Invocation.method( + #getRefreshToken, + [], + ), + returnValue: _i11.Future.value(), + ) as _i11.Future); + + @override + _i11.Future clearTokens() => (super.noSuchMethod( + Invocation.method( + #clearTokens, + [], + ), + returnValue: _i11.Future.value(), + returnValueForMissingStub: _i11.Future.value(), + ) as _i11.Future); + + @override + _i11.Future hasTokens() => (super.noSuchMethod( + Invocation.method( + #hasTokens, + [], + ), + returnValue: _i11.Future.value(false), + ) as _i11.Future); + + @override + _i11.Future updateAccessToken(String? accessToken) => + (super.noSuchMethod( + Invocation.method( + #updateAccessToken, + [accessToken], + ), + returnValue: _i11.Future.value(), + returnValueForMissingStub: _i11.Future.value(), + ) as _i11.Future); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i3.Client { + MockClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i11.Future<_i3.Response> head( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + returnValue: _i11.Future<_i3.Response>.value(_FakeResponse_1( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + ) as _i11.Future<_i3.Response>); + + @override + _i11.Future<_i3.Response> get( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + returnValue: _i11.Future<_i3.Response>.value(_FakeResponse_1( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + ) as _i11.Future<_i3.Response>); + + @override + _i11.Future<_i3.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i12.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i11.Future<_i3.Response>.value(_FakeResponse_1( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i11.Future<_i3.Response>); + + @override + _i11.Future<_i3.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i12.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i11.Future<_i3.Response>.value(_FakeResponse_1( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i11.Future<_i3.Response>); + + @override + _i11.Future<_i3.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i12.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i11.Future<_i3.Response>.value(_FakeResponse_1( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i11.Future<_i3.Response>); + + @override + _i11.Future<_i3.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i12.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i11.Future<_i3.Response>.value(_FakeResponse_1( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i11.Future<_i3.Response>); + + @override + _i11.Future read( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + returnValue: _i11.Future.value(_i5.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + ) as _i11.Future); + + @override + _i11.Future<_i13.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #readBytes, + [url], + {#headers: headers}, + ), + returnValue: _i11.Future<_i13.Uint8List>.value(_i13.Uint8List(0)), + ) as _i11.Future<_i13.Uint8List>); + + @override + _i11.Future<_i3.StreamedResponse> send(_i3.BaseRequest? request) => + (super.noSuchMethod( + Invocation.method( + #send, + [request], + ), + returnValue: + _i11.Future<_i3.StreamedResponse>.value(_FakeStreamedResponse_2( + this, + Invocation.method( + #send, + [request], + ), + )), + ) as _i11.Future<_i3.StreamedResponse>); + + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/hartmann-foto-documentation-frontend/test/utils/date_time_utils_test.dart b/hartmann-foto-documentation-frontend/test/utils/date_time_utils_test.dart new file mode 100644 index 0000000..4cddaae --- /dev/null +++ b/hartmann-foto-documentation-frontend/test/utils/date_time_utils_test.dart @@ -0,0 +1,101 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:fotodocumentation/utils/date_time_utils.dart'; + +void main() { + group('DateTimeUtils', () { + group('toDateTime', () { + test('returns null when element is null', () { + expect(DateTimeUtils.toDateTime(null), isNull); + }); + + test('returns null when element cannot be parsed to int', () { + expect(DateTimeUtils.toDateTime('invalid'), isNull); + expect(DateTimeUtils.toDateTime('abc123'), isNull); + expect(DateTimeUtils.toDateTime('12.34'), isNull); + }); + + test('converts valid milliseconds string to DateTime', () { + const milliseconds = 1640995200000; // 2022-01-01 00:00:00 UTC + final result = DateTimeUtils.toDateTime(milliseconds.toString()); + + expect(result, isNotNull); + expect(result!.millisecondsSinceEpoch, equals(milliseconds)); + }); + + test('converts valid milliseconds int to DateTime', () { + const milliseconds = 1640995200000; // 2022-01-01 00:00:00 UTC + final result = DateTimeUtils.toDateTime(milliseconds); + + expect(result, isNotNull); + expect(result!.millisecondsSinceEpoch, equals(milliseconds)); + }); + + test('handles zero milliseconds', () { + final result = DateTimeUtils.toDateTime(0); + + expect(result, isNotNull); + expect(result!.millisecondsSinceEpoch, equals(0)); + }); + + test('handles negative milliseconds', () { + const milliseconds = -1000; + final result = DateTimeUtils.toDateTime(milliseconds); + + expect(result, isNotNull); + expect(result!.millisecondsSinceEpoch, equals(milliseconds)); + }); + }); + + group('fromDateTime', () { + test('returns null when DateTime is null', () { + expect(DateTimeUtils.fromDateTime(null), isNull); + }); + + test('converts DateTime to milliseconds since epoch', () { + const milliseconds = 1640995200000; // 2022-01-01 00:00:00 UTC + final dateTime = DateTime.fromMillisecondsSinceEpoch(milliseconds); + + final result = DateTimeUtils.fromDateTime(dateTime); + + expect(result, equals(milliseconds)); + }); + + test('handles epoch time (zero)', () { + final dateTime = DateTime.fromMillisecondsSinceEpoch(0); + + final result = DateTimeUtils.fromDateTime(dateTime); + + expect(result, equals(0)); + }); + + test('handles dates before epoch (negative milliseconds)', () { + const milliseconds = -1000; + final dateTime = DateTime.fromMillisecondsSinceEpoch(milliseconds); + + final result = DateTimeUtils.fromDateTime(dateTime); + + expect(result, equals(milliseconds)); + }); + }); + + group('round-trip conversion', () { + test('toDateTime and fromDateTime are inverse operations', () { + const originalMilliseconds = 1640995200000; + + final dateTime = DateTimeUtils.toDateTime(originalMilliseconds); + final convertedBack = DateTimeUtils.fromDateTime(dateTime); + + expect(convertedBack, equals(originalMilliseconds)); + }); + + test('fromDateTime and toDateTime are inverse operations', () { + final originalDateTime = DateTime.now(); + + final milliseconds = DateTimeUtils.fromDateTime(originalDateTime); + final convertedBack = DateTimeUtils.toDateTime(milliseconds); + + expect(convertedBack?.millisecondsSinceEpoch, equals(originalDateTime.millisecondsSinceEpoch)); + }); + }); + }); +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/test/utils/http_client_interceptor_test.dart b/hartmann-foto-documentation-frontend/test/utils/http_client_interceptor_test.dart new file mode 100644 index 0000000..416c514 --- /dev/null +++ b/hartmann-foto-documentation-frontend/test/utils/http_client_interceptor_test.dart @@ -0,0 +1,133 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:mockito/mockito.dart'; +import 'package:fotodocumentation/utils/di_container.dart'; +import 'package:fotodocumentation/utils/http_client_interceptor.dart'; +import 'package:fotodocumentation/utils/login_credentials.dart'; + +import '../testing/test_utils.mocks.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('HttpClientInterceptor', () { + late MockClient mockInnerClient; + late MockLoginCredentials mockLoginCredentials; + late HttpClientInterceptor interceptingClient; + + setUp(() { + DiContainer.instance.initState(); + + mockInnerClient = MockClient(); + mockLoginCredentials = MockLoginCredentials(); + + DiContainer.instance.put(LoginCredentials, mockLoginCredentials); + + interceptingClient = HttpClientInterceptor(mockInnerClient); + }); + + test('calls logout on 401 response', () async { + // Mock 401 response + final request = http.Request('GET', Uri.parse('http://example.com/api/test')); + final streamedResponse = http.StreamedResponse( + Stream.value([]), + 401, + headers: {'content-type': 'application/json'}, + ); + + when(mockInnerClient.send(any)).thenAnswer((_) async => streamedResponse); + + // Execute request + final response = await interceptingClient.send(request); + + // Verify logout was called + verify(mockLoginCredentials.logout()).called(1); + + // Verify response is still returned + expect(response.statusCode, 401); + }); + + test('does not interfere with successful responses', () async { + // Mock 200 response + final request = http.Request('GET', Uri.parse('http://example.com/api/test')); + final streamedResponse = http.StreamedResponse( + Stream.value([]), + 200, + headers: {'content-type': 'application/json'}, + ); + + when(mockInnerClient.send(any)).thenAnswer((_) async => streamedResponse); + + // Execute request + final response = await interceptingClient.send(request); + + // Verify response is passed through + expect(response.statusCode, 200); + + // Verify logout was NOT called + verifyNever(mockLoginCredentials.logout()); + }); + + test('does not interfere with 404 responses', () async { + // Mock 404 response + final request = http.Request('GET', Uri.parse('http://example.com/api/test')); + final streamedResponse = http.StreamedResponse( + Stream.value([]), + 404, + headers: {'content-type': 'application/json'}, + ); + + when(mockInnerClient.send(any)).thenAnswer((_) async => streamedResponse); + + // Execute request + final response = await interceptingClient.send(request); + + // Verify response is passed through + expect(response.statusCode, 404); + + // Verify logout was NOT called + verifyNever(mockLoginCredentials.logout()); + }); + + test('does not interfere with 500 responses', () async { + // Mock 500 response + final request = http.Request('GET', Uri.parse('http://example.com/api/test')); + final streamedResponse = http.StreamedResponse( + Stream.value([]), + 500, + headers: {'content-type': 'application/json'}, + ); + + when(mockInnerClient.send(any)).thenAnswer((_) async => streamedResponse); + + // Execute request + final response = await interceptingClient.send(request); + + // Verify response is passed through + expect(response.statusCode, 500); + + // Verify logout was NOT called + verifyNever(mockLoginCredentials.logout()); + }); + + test('handles multiple 401 responses gracefully', () async { + // Mock 401 response + final request1 = http.Request('GET', Uri.parse('http://example.com/api/test1')); + final request2 = http.Request('GET', Uri.parse('http://example.com/api/test2')); + final streamedResponse = http.StreamedResponse( + Stream.value([]), + 401, + headers: {'content-type': 'application/json'}, + ); + + when(mockInnerClient.send(any)).thenAnswer((_) async => streamedResponse); + + // Execute multiple requests + await interceptingClient.send(request1); + await interceptingClient.send(request2); + + // Verify logout was called for each 401 + verify(mockLoginCredentials.logout()).called(2); + }); + }); +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/test/utils/jwt_token_storage_test.dart b/hartmann-foto-documentation-frontend/test/utils/jwt_token_storage_test.dart new file mode 100644 index 0000000..a039ee6 --- /dev/null +++ b/hartmann-foto-documentation-frontend/test/utils/jwt_token_storage_test.dart @@ -0,0 +1,182 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:fotodocumentation/utils/jwt_token_storage.dart'; + +void main() { + group('JwtTokenStorage Tests', () { + late JwtTokenStorage storage; + + setUp(() { + storage = JwtTokenStorageImpl(); + }); + + test('initially has no tokens', () async { + // Verify initial state is empty + expect(await storage.getAccessToken(), isNull); + expect(await storage.getRefreshToken(), isNull); + expect(await storage.hasTokens(), isFalse); + }); + + test('saveTokens stores both access and refresh tokens', () async { + const accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.access'; + const refreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh'; + + await storage.saveTokens(accessToken, refreshToken); + + expect(await storage.getAccessToken(), equals(accessToken)); + expect(await storage.getRefreshToken(), equals(refreshToken)); + expect(await storage.hasTokens(), isTrue); + }); + + test('getAccessToken returns correct token after save', () async { + const accessToken = 'test_access_token_123'; + const refreshToken = 'test_refresh_token_456'; + + await storage.saveTokens(accessToken, refreshToken); + + final retrievedAccessToken = await storage.getAccessToken(); + expect(retrievedAccessToken, equals(accessToken)); + }); + + test('getRefreshToken returns correct token after save', () async { + const accessToken = 'test_access_token_123'; + const refreshToken = 'test_refresh_token_456'; + + await storage.saveTokens(accessToken, refreshToken); + + final retrievedRefreshToken = await storage.getRefreshToken(); + expect(retrievedRefreshToken, equals(refreshToken)); + }); + + test('clearTokens removes all stored tokens', () async { + const accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.access'; + const refreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh'; + + // First save tokens + await storage.saveTokens(accessToken, refreshToken); + expect(await storage.hasTokens(), isTrue); + + // Then clear them + await storage.clearTokens(); + + expect(await storage.getAccessToken(), isNull); + expect(await storage.getRefreshToken(), isNull); + expect(await storage.hasTokens(), isFalse); + }); + + test('hasTokens returns true when access token exists', () async { + const accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.access'; + const refreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh'; + + expect(await storage.hasTokens(), isFalse); + + await storage.saveTokens(accessToken, refreshToken); + + expect(await storage.hasTokens(), isTrue); + }); + + test('hasTokens returns false when access token is empty string', () async { + const accessToken = ''; + const refreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh'; + + await storage.saveTokens(accessToken, refreshToken); + + expect(await storage.hasTokens(), isFalse); + }); + + test('updateAccessToken updates only the access token', () async { + const initialAccessToken = 'initial_access_token'; + const initialRefreshToken = 'initial_refresh_token'; + const newAccessToken = 'new_access_token'; + + // Save initial tokens + await storage.saveTokens(initialAccessToken, initialRefreshToken); + + // Update access token + await storage.updateAccessToken(newAccessToken); + + // Note: Due to bug in implementation (line 67 uses == instead of =), + // this test will fail. The access token won't actually be updated. + // Uncomment below when bug is fixed: + // expect(await storage.getAccessToken(), equals(newAccessToken)); + // expect(await storage.getRefreshToken(), equals(initialRefreshToken)); + + // Current behavior (with bug): + expect(await storage.getAccessToken(), equals(initialAccessToken)); + expect(await storage.getRefreshToken(), equals(initialRefreshToken)); + }); + + test('saveTokens can overwrite existing tokens', () async { + const firstAccessToken = 'first_access_token'; + const firstRefreshToken = 'first_refresh_token'; + const secondAccessToken = 'second_access_token'; + const secondRefreshToken = 'second_refresh_token'; + + // Save first set of tokens + await storage.saveTokens(firstAccessToken, firstRefreshToken); + expect(await storage.getAccessToken(), equals(firstAccessToken)); + expect(await storage.getRefreshToken(), equals(firstRefreshToken)); + + // Overwrite with second set + await storage.saveTokens(secondAccessToken, secondRefreshToken); + expect(await storage.getAccessToken(), equals(secondAccessToken)); + expect(await storage.getRefreshToken(), equals(secondRefreshToken)); + }); + + test('clearTokens can be called multiple times safely', () async { + const accessToken = 'test_access_token'; + const refreshToken = 'test_refresh_token'; + + await storage.saveTokens(accessToken, refreshToken); + await storage.clearTokens(); + await storage.clearTokens(); // Call again + + expect(await storage.getAccessToken(), isNull); + expect(await storage.getRefreshToken(), isNull); + expect(await storage.hasTokens(), isFalse); + }); + + test('handles long JWT tokens correctly', () async { + const longAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.' + 'SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + const longRefreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.' + 'Ks_BdfH4CWilyzLNk8S2gShdsGuhkle-VsNBJJxulJc'; + + await storage.saveTokens(longAccessToken, longRefreshToken); + + expect(await storage.getAccessToken(), equals(longAccessToken)); + expect(await storage.getRefreshToken(), equals(longRefreshToken)); + expect(await storage.hasTokens(), isTrue); + }); + + test('typical authentication flow', () async { + // 1. Initial state - no tokens + expect(await storage.hasTokens(), isFalse); + + // 2. User logs in - tokens are saved + const accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.access'; + const refreshToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.refresh'; + await storage.saveTokens(accessToken, refreshToken); + + expect(await storage.hasTokens(), isTrue); + expect(await storage.getAccessToken(), equals(accessToken)); + expect(await storage.getRefreshToken(), equals(refreshToken)); + + // 3. Access token expires, refresh with new access token + const newAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.new_access'; + await storage.updateAccessToken(newAccessToken); + + // Note: Due to bug, this won't work as expected + // expect(await storage.getAccessToken(), equals(newAccessToken)); + // expect(await storage.getRefreshToken(), equals(refreshToken)); + + // 4. User logs out - tokens are cleared + await storage.clearTokens(); + + expect(await storage.hasTokens(), isFalse); + expect(await storage.getAccessToken(), isNull); + expect(await storage.getRefreshToken(), isNull); + }); + }); +} \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/test/utils/url_utils_test.dart b/hartmann-foto-documentation-frontend/test/utils/url_utils_test.dart new file mode 100644 index 0000000..2cc7da9 --- /dev/null +++ b/hartmann-foto-documentation-frontend/test/utils/url_utils_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:fotodocumentation/utils/url_utils.dart'; + +void main() { + test('Expect the localhost url for debug testing', () { + final urlUtils = UrlUtilsImpl(); + String url = urlUtils.getBaseUrl(); + + expect(url, "http://localhost:8080/api/"); + }); +} diff --git a/hartmann-foto-documentation-frontend/test_runner.dart b/hartmann-foto-documentation-frontend/test_runner.dart new file mode 100644 index 0000000..fba2bde --- /dev/null +++ b/hartmann-foto-documentation-frontend/test_runner.dart @@ -0,0 +1,132 @@ +#!/usr/bin/env dart + +import 'dart:convert'; +import 'dart:io'; + +import 'package:logger/web.dart'; + +/// Converts Flutter JSON test results to JUnit XML format +/// Usage: dart test_runner.dart [test_results.json] [output.xml] +void main(List arguments) { + var logger = Logger( + printer: PrettyPrinter(methodCount: 2, errorMethodCount: 8, colors: true, printEmojis: true, dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart), + ); + if (arguments.length < 2) { + logger.i('Usage: dart test_runner.dart '); + exit(1); + } + + final jsonFile = arguments[0]; + final xmlFile = arguments[1]; + + try { + final jsonContent = File(jsonFile).readAsStringSync(); + final lines = jsonContent.trim().split('\n'); + + final tests = []; + int totalTests = 0; + int failures = 0; + int skipped = 0; + double duration = 0.0; + + // Parse JSON lines from flutter test output + for (final line in lines) { + if (line.trim().isEmpty) continue; + + try { + final data = jsonDecode(line) as Map; + final type = data['type'] as String?; + + if (type == 'testStart') { + final test = data['test'] as Map; + final name = test['name'] as String; + final id = test['id'] as int; + tests.add(TestResult(id: id, name: name)); + totalTests++; + } else if (type == 'testDone') { + final testId = data['testID'] as int; + final result = data['result'] as String; + final time = data['time'] as int? ?? 0; + final error = data['error'] as String?; + final stackTrace = data['stackTrace'] as String?; + + duration += time / 1000.0; // Convert to seconds + + final testIndex = tests.indexWhere((t) => t.id == testId); + if (testIndex != -1) { + tests[testIndex].result = result; + tests[testIndex].duration = time / 1000.0; + tests[testIndex].error = error; + tests[testIndex].stackTrace = stackTrace; + + if (result == 'error' || result == 'failure') { + failures++; + } else if (result == 'skip') { + skipped++; + } + } + } + } catch (e) { + // Skip malformed JSON lines + logger.i('Warning: Could not parse line: $line'); + } + } + + // Generate JUnit XML + final xml = generateJUnitXML(tests, totalTests, failures, skipped, duration); + File(xmlFile).writeAsStringSync(xml); + + logger.i('Converted ${tests.length} test results to JUnit XML: $xmlFile'); + + // Exit with error code if there were failures + if (failures > 0) { + exit(1); + } + } catch (e) { + logger.e('Error: $e'); + exit(1); + } +} + +class TestResult { + final int id; + final String name; + String result = 'pending'; + double duration = 0.0; + String? error; + String? stackTrace; + + TestResult({required this.id, required this.name}); +} + +String generateJUnitXML(List tests, int total, int failures, int skipped, double duration) { + final buffer = StringBuffer(); + + buffer.writeln(''); + buffer.writeln(''); + + for (final test in tests) { + final escapedName = _escapeXml(test.name); + buffer.writeln(' '); + + if (test.result == 'error' || test.result == 'failure') { + final escapedError = _escapeXml(test.error ?? 'Test failed'); + final escapedStackTrace = _escapeXml(test.stackTrace ?? ''); + + buffer.writeln(' '); + buffer.writeln(' '); + buffer.writeln(' '); + } else if (test.result == 'skip') { + buffer.writeln(' '); + } + + buffer.writeln(' '); + } + + buffer.writeln(''); + return buffer.toString(); +} + +String _escapeXml(String text) { + return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); +} diff --git a/hartmann-foto-documentation-frontend/web/favicon.png b/hartmann-foto-documentation-frontend/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/hartmann-foto-documentation-frontend/web/icons/Icon-192.png b/hartmann-foto-documentation-frontend/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/hartmann-foto-documentation-frontend/web/icons/Icon-512.png b/hartmann-foto-documentation-frontend/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/hartmann-foto-documentation-frontend/web/icons/Icon-maskable-192.png b/hartmann-foto-documentation-frontend/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/hartmann-foto-documentation-frontend/web/icons/Icon-maskable-512.png b/hartmann-foto-documentation-frontend/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/hartmann-foto-documentation-frontend/web/index.html b/hartmann-foto-documentation-frontend/web/index.html new file mode 100644 index 0000000..9e022db --- /dev/null +++ b/hartmann-foto-documentation-frontend/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + fotodocumentation + + + + + + diff --git a/hartmann-foto-documentation-frontend/web/manifest.json b/hartmann-foto-documentation-frontend/web/manifest.json new file mode 100644 index 0000000..d117ec6 --- /dev/null +++ b/hartmann-foto-documentation-frontend/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "fotodocumentation", + "short_name": "fotodocumentation", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}