added frontend
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<T>() {
|
||||
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<T>(Type key, T object) {
|
||||
_container[key] = object;
|
||||
}
|
||||
|
||||
T get2<T>() {
|
||||
return _container[T] as T;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
static final GlobalKey<NavigatorState> bottomBarNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'bottombar');
|
||||
static final GlobalKey<NavigatorState> adminNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'admin');
|
||||
static final GlobalKey<NavigatorState> skillEditorNavigatorKey = GlobalKey<NavigatorState>(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: <RouteBase>[
|
||||
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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
class GlobalStack<T> {
|
||||
final _list = <T>[];
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<http.StreamedResponse> 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>();
|
||||
loginCredentials.logout();
|
||||
|
||||
// Navigate to login page using GoRouter
|
||||
final context = GlobalRouter.rootNavigatorKey.currentContext;
|
||||
if (context != null) {
|
||||
context.go(GlobalRouter.pathLogin);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
@@ -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<void> saveTokens(String accessToken, String refreshToken);
|
||||
|
||||
/// Get the stored access token
|
||||
///
|
||||
/// @return Access token or null if not found
|
||||
Future<String?> getAccessToken();
|
||||
|
||||
/// Get the stored refresh token
|
||||
///
|
||||
/// @return Refresh token or null if not found
|
||||
Future<String?> getRefreshToken();
|
||||
|
||||
/// Clear all stored tokens (on logout)
|
||||
Future<void> clearTokens();
|
||||
|
||||
/// Check if tokens are stored
|
||||
///
|
||||
/// @return true if access token exists
|
||||
Future<bool> hasTokens();
|
||||
|
||||
/// Update only the access token (used after refresh)
|
||||
///
|
||||
/// @param accessToken New access token
|
||||
Future<void> updateAccessToken(String accessToken);
|
||||
}
|
||||
|
||||
class JwtTokenStorageImpl extends JwtTokenStorage {
|
||||
|
||||
// Storage keys
|
||||
String? _keyAccessToken;
|
||||
String? _keyRefreshToken;
|
||||
|
||||
@override
|
||||
Future<void> saveTokens(String accessToken, String refreshToken) async {
|
||||
_keyAccessToken = accessToken;
|
||||
_keyRefreshToken = refreshToken;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getAccessToken() async {
|
||||
return _keyAccessToken;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getRefreshToken() async {
|
||||
return _keyRefreshToken;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearTokens() async {
|
||||
_keyAccessToken = null;
|
||||
_keyRefreshToken = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> hasTokens() async {
|
||||
return _keyAccessToken != null && _keyAccessToken!.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateAccessToken(String accessToken) async {
|
||||
_keyAccessToken == accessToken;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<PointerDeviceKind> get dragDevices => {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
};
|
||||
}
|
||||
|
||||
class ThemeLoader {
|
||||
static Future<ThemeData> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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/";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user