Skip to content

Commit ead9549

Browse files
shinyfordslightfoot
authored andcommitted
fix: refactor password criteria matching [CLERK_SDK #20]
1 parent edbc466 commit ead9549

File tree

5 files changed

+68
-58
lines changed

5 files changed

+68
-58
lines changed

packages/clerk_auth/lib/models/environment/password_settings.dart

+21
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ part 'password_settings.g.dart';
66

77
@JsonSerializable(explicitToJson: true, fieldRename: FieldRename.snake)
88
class PasswordSettings {
9+
static final _lowerCaseRE = RegExp(r'[a-z]');
10+
static final _upperCaseRE = RegExp(r'[A-Z]');
11+
static final _numberRE = RegExp(r'[0-9]');
12+
913
static const empty = PasswordSettings();
1014

1115
final String allowedSpecialCharacters;
@@ -51,4 +55,21 @@ class PasswordSettings {
5155
factory PasswordSettings.fromJson(Map<String, dynamic> json) => _$PasswordSettingsFromJson(json);
5256

5357
Map<String, dynamic> toJson() => _$PasswordSettingsToJson(this);
58+
59+
bool meetsLowerCaseCriteria(String password) =>
60+
requireLowercase == false || _lowerCaseRE.hasMatch(password);
61+
bool meetsUpperCaseCriteria(String password) =>
62+
requireUppercase == false || _upperCaseRE.hasMatch(password);
63+
bool meetsNumberCriteria(String password) =>
64+
requireNumbers == false || _numberRE.hasMatch(password);
65+
66+
bool meetsSpecialCharCriteria(String password) =>
67+
requireSpecialChar == false ||
68+
allowedSpecialCharacters.runes.toSet().intersection(password.runes.toSet()).isNotEmpty;
69+
70+
bool meetsRequiredCriteria(String password) =>
71+
meetsLowerCaseCriteria(password) &&
72+
meetsUpperCaseCriteria(password) &&
73+
meetsNumberCriteria(password) &&
74+
meetsSpecialCharCriteria(password);
5475
}

packages/clerk_flutter/example/lib/main.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class MainApp extends StatelessWidget {
2727
padding: horizontalPadding32,
2828
child: Center(
2929
child: ClerkAuthBuilder(
30-
signedInBuilder: (context, auth) => ClerkUserButton(),
30+
signedInBuilder: (context, auth) => const ClerkUserButton(),
3131
signedOutBuilder: (context, auth) => const ClerkAuthenticationWidget(),
3232
),
3333
),

packages/clerk_flutter/lib/src/widgets/control/clerk_auth.dart

+10-32
Original file line numberDiff line numberDiff line change
@@ -136,22 +136,19 @@ class _ClerkAuthData extends InheritedWidget {
136136

137137
class ClerkAuthProvider extends Clerk.Auth with ChangeNotifier {
138138
static const _kRotatingTokenNonce = 'rotating_token_nonce';
139-
static final _lowerCaseRE = RegExp(r'[a-z]');
140-
static final _upperCaseRE = RegExp(r'[A-Z]');
141-
static final _numberRE = RegExp(r'[0-9]');
142139

143140
final ClerkTranslator translator;
144141
final _errors = StreamController<Clerk.AuthError>();
145142
late final errorStream = _errors.stream.asBroadcastStream();
146-
final OverlayEntry loadingOverlay;
143+
final OverlayEntry _loadingOverlay;
147144

148145
ClerkAuthProvider({
149146
required super.publicKey,
150147
required super.publishableKey,
151148
this.translator = const DefaultClerkTranslator(),
152149
Widget? loading,
153150
super.persistor,
154-
}) : this.loadingOverlay = OverlayEntry(builder: (context) => loading ?? defaultLoadingWidget);
151+
}) : _loadingOverlay = OverlayEntry(builder: (context) => loading ?? defaultLoadingWidget);
155152

156153
@override
157154
void update() => notifyListeners();
@@ -216,13 +213,13 @@ class ClerkAuthProvider extends Clerk.Auth with ChangeNotifier {
216213
}) async {
217214
T? result;
218215
try {
219-
Overlay.of(context).insert(loadingOverlay);
216+
Overlay.of(context).insert(_loadingOverlay);
220217
result = await fn.call();
221218
} on Clerk.AuthError catch (error) {
222219
_errors.add(error);
223220
onError?.call(error);
224221
} finally {
225-
loadingOverlay.remove();
222+
_loadingOverlay.remove();
226223
}
227224
return result;
228225
}
@@ -232,23 +229,7 @@ class ClerkAuthProvider extends Clerk.Auth with ChangeNotifier {
232229
bool passwordIsValid(String? password, String? confirmation) {
233230
if (password case String password when password.isNotEmpty) {
234231
if (password != confirmation) return false;
235-
236-
final criteria = env.user.passwordSettings;
237-
238-
if (criteria.requireLowercase && _lowerCaseRE.hasMatch(password) == false) return false;
239-
240-
if (criteria.requireUppercase && _upperCaseRE.hasMatch(password) == false) return false;
241-
242-
if (criteria.requireNumbers && _numberRE.hasMatch(password) == false) return false;
243-
244-
if (criteria.requireSpecialChar) {
245-
final pRunes = password.runes.toSet();
246-
final scRunes = criteria.allowedSpecialCharacters.runes.toSet();
247-
final lacksSpecialChar = pRunes.intersection(scRunes).isEmpty;
248-
if (lacksSpecialChar) return false;
249-
}
250-
251-
return true;
232+
return env.user.passwordSettings.meetsRequiredCriteria(password);
252233
}
253234

254235
return false;
@@ -267,23 +248,20 @@ class ClerkAuthProvider extends Clerk.Auth with ChangeNotifier {
267248
final criteria = env.user.passwordSettings;
268249
final missing = <String>[];
269250

270-
if (criteria.requireLowercase && _lowerCaseRE.hasMatch(password) == false) {
251+
if (criteria.meetsLowerCaseCriteria(password) == false) {
271252
missing.add('a LOWERCASE letter');
272253
}
273254

274-
if (criteria.requireUppercase && _upperCaseRE.hasMatch(password) == false) {
255+
if (criteria.meetsUpperCaseCriteria(password) == false) {
275256
missing.add('an UPPERCASE letter');
276257
}
277258

278-
if (criteria.requireNumbers && _numberRE.hasMatch(password) == false) {
259+
if (criteria.meetsNumberCriteria(password) == false) {
279260
missing.add('a NUMBER');
280261
}
281262

282-
if (criteria.requireSpecialChar) {
283-
final pRunes = password.runes.toSet();
284-
final scRunes = criteria.allowedSpecialCharacters.runes.toSet();
285-
final lacksSpecialChar = pRunes.intersection(scRunes).isEmpty;
286-
if (lacksSpecialChar) missing.add('a SPECIAL CHARACTER (###)');
263+
if (criteria.meetsSpecialCharCriteria(password) == false) {
264+
missing.add('a SPECIAL CHARACTER (###)');
287265
}
288266

289267
if (missing.isNotEmpty) {

packages/clerk_flutter/lib/src/widgets/user/clerk_user_button.dart

+32-22
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,34 @@ import 'package:clerk_flutter/clerk_flutter.dart';
55
import 'package:common/common.dart';
66
import 'package:flutter/material.dart';
77

8-
class ClerkUserButton extends StatelessWidget {
9-
static const _closeDelay = Duration(milliseconds: 250);
10-
8+
class ClerkUserButton extends StatefulWidget {
119
final bool showName;
1210

13-
ClerkUserButton({super.key, this.showName = true});
11+
const ClerkUserButton({super.key, this.showName = true});
12+
13+
@override
14+
State<ClerkUserButton> createState() => _ClerkUserButtonState();
15+
}
16+
17+
class _ClerkUserButtonState extends State<ClerkUserButton> {
18+
static const _closeDelay = Duration(milliseconds: 250);
1419

1520
final _sessions = <Clerk.Session>[];
1621

22+
Future<void> _manage(List<Clerk.Session> sessions) async {
23+
// adding to a list of existing sessions means we have ones that are now deleted
24+
// still available in `_sessions`, making for prettier UI
25+
_sessions.addOrReplaceAll(sessions, by: (s) => s.id);
26+
27+
// after a delay period, all deleted sessions will have been closed, so we can
28+
// clear the `_sessions` cache of any such for next time round
29+
// Using `removeWhere` maintains the list order in `_sessions`, stopping weird
30+
// UI jumping as arbitrary list order is imposed
31+
await Future.delayed(_closeDelay);
32+
33+
_sessions.removeWhere((s) => sessions.contains(s) == false);
34+
}
35+
1736
@override
1837
Widget build(BuildContext context) {
1938
return DecoratedBox(
@@ -26,19 +45,7 @@ class ClerkUserButton extends StatelessWidget {
2645
final translator = auth.translator;
2746
final sessions = auth.client.sessions;
2847

29-
// adding to a list of existing sessions means we have ones that are now deleted
30-
// still available in `_sessions`, making for prettier UI
31-
_sessions.addOrReplaceAll(sessions, by: (s) => s.id);
32-
33-
// after a delay period, all deleted sessions will have been closed, so we can
34-
// clear the `_sessions` cache of any such for next time round
35-
//
36-
// Using `removeWhere` maintains the list order in `_sessions`, stopping weird
37-
// UI jumping as arbitrary list order is imposed
38-
Future.delayed(
39-
_closeDelay,
40-
() => _sessions.removeWhere((s) => sessions.contains(s) == false),
41-
);
48+
_manage(sessions);
4249

4350
return ClerkVerticalCard(
4451
topPortion: Column(
@@ -48,9 +55,9 @@ class ClerkUserButton extends StatelessWidget {
4855
_SessionRow(
4956
key: Key(session.id),
5057
session: session,
51-
closed: auth.client.sessions.contains(session) == false,
58+
closed: sessions.contains(session) == false,
5259
selected: session == auth.client.activeSession,
53-
showName: showName,
60+
showName: widget.showName,
5461
onTap: () => auth.call(context, () => auth.setActiveSession(session)),
5562
),
5663
if (auth.env.config.singleSessionMode == false)
@@ -82,7 +89,7 @@ class ClerkUserButton extends StatelessWidget {
8289
],
8390
),
8491
bottomPortion: Closeable(
85-
open: auth.client.sessions.length > 1,
92+
open: sessions.length > 1,
8693
child: Padding(
8794
padding: horizontalPadding16 + verticalPadding12,
8895
child: GestureDetector(
@@ -191,7 +198,7 @@ class _DottedBorderPainter extends CustomPainter {
191198
static const _twoPi = 2 * math.pi;
192199

193200
final double dashLength;
194-
final double gapLength;
201+
final double gapLength; // actually, minimum gap length
195202

196203
final Paint _paint;
197204
final Paint _backgroundPaint;
@@ -201,7 +208,10 @@ class _DottedBorderPainter extends CustomPainter {
201208
required Color backgroundColor,
202209
this.dashLength = 0,
203210
this.gapLength = 0,
204-
}) : assert(dashLength > 0 || gapLength == 0, 'dashLength cannot be 0 unless gapLength is 0'),
211+
}) : assert(
212+
dashLength > 0 || gapLength == 0,
213+
'dashLength cannot be 0 or less unless gapLength is 0',
214+
),
205215
_paint = Paint()
206216
..style = PaintingStyle.stroke
207217
..color = color,

packages/common/lib/src/extensions.dart

+4-3
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ extension StringExtension on String {
3232
}
3333

3434
extension ListExtension on List {
35-
void addOrReplaceAll<T, S>(Iterable<T> list, {S Function(T)? by}) {
35+
void addOrReplaceAll<T>(Iterable<T> list, {dynamic Function(T)? by}) {
36+
by ??= (t) => t;
3637
for (final item in list) {
37-
final identifier = by?.call(item) ?? item;
38-
switch (indexWhere((i) => (by?.call(i) ?? i) == identifier)) {
38+
final identifier = by.call(item);
39+
switch (indexWhere((i) => by!.call(i) == identifier)) {
3940
case int idx when idx > -1:
4041
this[idx] = item;
4142
default:

0 commit comments

Comments
 (0)