Skip to content

Commit c59e18a

Browse files
author
Lukas Gundermann
committed
feat: Add 403 "Not Authorized" error page
This commit introduces a new, standalone error page for handling "403 Not Authorized" scenarios. The page is designed to be visually consistent with the existing 404 page and provides clear information and navigation options for the user. ### Key Changes: * **`NotAuthorizedScreen` and `NotAuthorizedCard`:** * A new screen (`NotAuthorizedScreen`) and a corresponding card widget (`NotAuthorizedCard`) have been created to display the 403 error. * The card features a "403 Zugriff verweigert" title, a descriptive message, and an "Access Information" section detailing common reasons for the error (e.g., insufficient permissions). * Navigation buttons allow the user to go back to the previous page or return to the start page. * **Routing:** * A new route (`/not-authorized`, named `not-authorized`) has been added to the router configuration to display the `NotAuthorizedScreen`. * **Configuration:** * The `ErrorSettings` class is updated with new static lists and properties for the 403 page's content, such as informational messages and the link to the home page. * **`IconContainer` Refactor:** * The `IconContainer` widget is refactored to accept a dynamic `IconData` property, making it more reusable. It was previously hardcoded with `LucideIcons.fileQuestion`. * **Navigation:** * A new entry, "403 Nicht autorisiert - Standalone," has been added to the "Error Pages" section in the navigation for demonstration and testing purposes.
1 parent b0180cc commit c59e18a

8 files changed

Lines changed: 214 additions & 4 deletions

File tree

lib/routes/route_config.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:talker_flutter/talker_flutter.dart';
1111

1212
import '../bloc/auth/auth_bloc.dart';
1313
import '../logs/route_log.dart';
14+
import '../screens/error_pages/not_authorized_screen.dart';
1415
import '../screens/widgets/layouts/main_layout.dart';
1516
import 'cubit/router_cubit.dart';
1617

@@ -139,6 +140,11 @@ class RouteConfig {
139140
path: RouteNames.notFoundUrl,
140141
builder: (context, state) => NotFoundScreen(),
141142
),
143+
GoRoute(
144+
name: RouteNames.notAuthorized,
145+
path: RouteNames.notAuthorizedUrl,
146+
builder: (context, state) => NotAuthorizedScreen(),
147+
),
142148
StatefulShellRoute.indexedStack(
143149
/// Provides a persistent shell layout (e.g. app bar) around branches.
144150
builder: (context, state, navigationShell) =>

lib/routes/route_names.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class RouteNames {
99
static const String notFound = "not-found";
1010
static const String notFoundUrl = '/$notFound';
1111

12+
static const String notAuthorized = "not-authorized";
13+
static const String notAuthorizedUrl = '/$notAuthorized';
14+
1215
// Components
1316
static const String buttons = "buttons";
1417
static const String buttonsUrl = '/$buttons';
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'package:flutter_bloc/flutter_bloc.dart';
2+
import 'package:police_flutter_template/screens/widgets/error_pages/not_authorized_card.dart';
3+
import 'package:shadcn_flutter/shadcn_flutter.dart';
4+
5+
import '../../theme/cubit/theme_cubit.dart';
6+
import '../widgets/layouts/full_screen_layout.dart';
7+
8+
class NotAuthorizedScreen extends StatelessWidget {
9+
const NotAuthorizedScreen({super.key});
10+
11+
@override
12+
Widget build(BuildContext context) {
13+
final isDarkMode = context.watch<ThemeCubit>().state.isDarkMode;
14+
return FullScreenLayout(
15+
decoration: BoxDecoration(
16+
gradient: LinearGradient(
17+
begin: Alignment.topLeft,
18+
end: Alignment.bottomRight,
19+
colors: isDarkMode
20+
? [Colors.gray[900], Colors.gray[950]]
21+
: [Colors.orange[50], Colors.orange[100]],
22+
),
23+
),
24+
children: [
25+
NotAuthorizedCard(
26+
withShadow: true,
27+
color: Colors.orange[300],
28+
darkColor: Colors.orange[600],
29+
),
30+
],
31+
);
32+
}
33+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import 'package:flutter_bloc/flutter_bloc.dart';
2+
import 'package:go_router/go_router.dart';
3+
import 'package:shadcn_flutter/shadcn_flutter.dart';
4+
5+
import '../../../extensions/text_extensions.dart';
6+
import '../../../settings/error_settings.dart';
7+
import '../../../theme/cubit/theme_cubit.dart';
8+
import '../icon_container.dart';
9+
10+
class NotAuthorizedCard extends StatelessWidget {
11+
const NotAuthorizedCard({
12+
super.key,
13+
this.accessInformations,
14+
this.color,
15+
this.darkColor,
16+
this.withShadow = false,
17+
});
18+
19+
final List<String>? accessInformations;
20+
21+
final bool withShadow;
22+
23+
final Color? color;
24+
25+
final Color? darkColor;
26+
27+
@override
28+
Widget build(BuildContext context) {
29+
final isDarkMode = context.watch<ThemeCubit>().state.isDarkMode;
30+
return ConstrainedBox(
31+
constraints: BoxConstraints(maxWidth: 720),
32+
child: Card(
33+
boxShadow: withShadow
34+
? [
35+
isDarkMode
36+
? BoxShadow(
37+
color: Color.fromRGBO(255, 255, 255, 0.25),
38+
offset: Offset(0, 0),
39+
blurRadius: 50,
40+
spreadRadius: -12,
41+
)
42+
: BoxShadow(
43+
color: Color.fromRGBO(0, 0, 0, 0.25),
44+
offset: Offset(0, 0),
45+
blurRadius: 50,
46+
spreadRadius: -12,
47+
),
48+
]
49+
: null,
50+
borderColor: isDarkMode ? darkColor : color,
51+
borderWidth: 2,
52+
child: Column(
53+
children: [
54+
IconContainer(
55+
color: Colors.orange[100],
56+
darkColor: Colors.orange[900].withAlpha(100),
57+
icon: LucideIcons.shieldAlert,
58+
iconColor: Colors.orange[600],
59+
iconDarkColor: Colors.orange[400],
60+
),
61+
Gap(24),
62+
Text('403').bold.x8Large.setColors(
63+
lightColor: Colors.orange[600],
64+
darkColor: Colors.orange[400],
65+
),
66+
Gap(8),
67+
Text('Zugriff verweigert').bold.x3Large.setColors(
68+
lightColor: Colors.gray[900],
69+
darkColor: Colors.white,
70+
),
71+
Gap(16),
72+
ConstrainedBox(
73+
constraints: BoxConstraints(maxWidth: 440),
74+
child:
75+
Text(
76+
'Sie haben keine Berechtigung, auf diese Seite oder Ressource zuzugreifen.',
77+
textAlign: TextAlign.center,
78+
).large.setColors(
79+
lightColor: Colors.gray[600],
80+
darkColor: Colors.gray[400],
81+
),
82+
),
83+
Gap(24),
84+
SizedBox(
85+
width: double.infinity,
86+
child: Card(
87+
padding: EdgeInsets.all(24),
88+
filled: true,
89+
fillColor: isDarkMode
90+
? Colors.orange[900].withAlpha(25)
91+
: Colors.orange[50],
92+
child: Column(
93+
crossAxisAlignment: CrossAxisAlignment.start,
94+
children: [
95+
Row(
96+
mainAxisAlignment: MainAxisAlignment.center,
97+
children: [
98+
Icon(
99+
LucideIcons.lock,
100+
color: isDarkMode
101+
? Colors.orange[400]
102+
: Colors.orange[600],
103+
),
104+
Gap(10),
105+
Text('Zugriffsinformationen').h4.setColors(
106+
lightColor: Colors.gray[900],
107+
darkColor: Colors.white,
108+
),
109+
],
110+
),
111+
Gap(12),
112+
Column(
113+
crossAxisAlignment: CrossAxisAlignment.start,
114+
children:
115+
(accessInformations ??
116+
ErrorSettings.accessInformations)
117+
.map(
118+
(reason) => Text(reason).li.setColors(
119+
lightColor: Colors.gray[600],
120+
darkColor: Colors.gray[400],
121+
),
122+
)
123+
.toList(),
124+
).gap(5),
125+
],
126+
),
127+
),
128+
).withPadding(horizontal: 30),
129+
Gap(32),
130+
Row(
131+
mainAxisAlignment: MainAxisAlignment.center,
132+
children: [
133+
OutlineButton(
134+
onPressed: () => context.pop(),
135+
enabled: context.canPop(),
136+
leading: const Icon(LucideIcons.arrowLeft),
137+
child: Text('Zurück'),
138+
),
139+
PrimaryButton(
140+
onPressed: () =>
141+
context.goNamed(ErrorSettings.accessInformationHomeLink),
142+
leading: const Icon(LucideIcons.house),
143+
child: Text('Zur Startseite'),
144+
),
145+
],
146+
).gap(10),
147+
Divider().withPadding(all: 32),
148+
Text('Benötigen Sie Hilfe? Kontaktieren Sie uns.').muted.small,
149+
],
150+
).withPadding(vertical: 48),
151+
),
152+
);
153+
}
154+
}

lib/screens/widgets/error_pages/not_found_card.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ class NotFoundCard extends StatelessWidget {
125125
),
126126
PrimaryButton(
127127
onPressed: () =>
128-
context.pushNamed(ErrorSettings.notFoundHomeLink),
128+
context.goNamed(ErrorSettings.notFoundHomeLink),
129129
leading: const Icon(LucideIcons.house),
130130
child: Text('Zur Startseite'),
131131
),

lib/screens/widgets/icon_container.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class IconContainer extends StatelessWidget {
4343
borderRadius: BorderRadius.circular(999),
4444
),
4545
child: Icon(
46-
LucideIcons.fileQuestion,
46+
icon,
4747
size: 48,
4848
color: darkColor != null && isDarkMode ? iconDarkColor : iconColor,
4949
),

lib/settings/error_settings.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,12 @@ class ErrorSettings {
88
"Die Seite wurde gelöscht oder verschoben",
99
"Der Link ist veraltet oder ungültig",
1010
];
11-
}
11+
12+
static String accessInformationHomeLink = RouteNames.home;
13+
14+
static List<String> accessInformations = [
15+
"Diese Seite ist nur für autorisierte Benutzer zugänglich",
16+
"Ihre Anmeldedaten haben nicht die erforderlichen Berechtigungen",
17+
"Wenden Sie sich an Ihren Administrator für Zugriff",
18+
];
19+
}

lib/settings/navigation_items.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,16 @@ class NavigationItems {
5858
),
5959
NavigationItemModel(
6060
index: null,
61-
title: "Not Found - Standalone",
61+
title: "404 Nicht gefunden - Standalone",
6262
icon: const Icon(LucideIcons.fileQuestion),
6363
onPressed: () => context.pushNamed(RouteNames.notFound),
6464
),
65+
NavigationItemModel(
66+
index: null,
67+
title: "403 Nicht autorisiert - Standalone",
68+
icon: const Icon(LucideIcons.fileQuestion),
69+
onPressed: () => context.pushNamed(RouteNames.notAuthorized),
70+
),
6571
],
6672
),
6773
NavigationItemModel(

0 commit comments

Comments
 (0)