Skip to content

Commit ba52678

Browse files
committed
feat(auth): update password reset page UI and policy
- Replace email field outline with filled/disabled style - Add password visibility toggle with eye icon - Add confirm field match indicator (empty circle / green check / red X) - Add password requirements list derived from config - Add help link below the card - Disable submit button until all requirements pass and fields match - Update card title and subheader to FN branding - Fix min password length to 10 (was 8) and add + to allowed special chars pattern - Add AUTH_PASSWORD_MIN_LENGTH, AUTH_PASSWORD_SHAPE_LIST, and AUTH_PASSWORD_ALLOWED_SPECIAL_CHARACTERS_TEXT config keys - Update local dev docs: queue drain command and config cache notes
1 parent 83eaa4a commit ba52678

6 files changed

Lines changed: 153 additions & 17 deletions

File tree

.env.example

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ MAIL_SEND_WELCOME_EMAIL=1
110110
DEFAULT_PROFILE_IMAGE=
111111

112112
AUTH_PASSWORD_RESET_LIFETIME=1800
113-
114113
AUTH_PASSWORD_SHAPE_PATTERN="^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-])"
115114
AUTH_PASSWORD_SHAPE_WARNING="Password must include at least one uppercase letter, one lowercase letter, one number, and one special character."
116115

LOCAL_DEVELOPMENT_HOWTO.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,56 @@ SSL_ENABLED=false
2929
[https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-compose-on-ubuntu-22-04](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-compose-on-ubuntu-22-04) and [https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04)
3030
4.Run script ./start_local_server.sh (http://localhost:8001/)
3131

32+
Frontend Development
33+
====================
34+
35+
openstackid is a Laravel app — blade templates reference compiled JS bundles in `public/assets/`, not source files. The source files in `resources/js/` must be compiled before changes take effect in the browser.
36+
37+
**One-time setup:** The compiled assets in `public/assets/` are owned by root if they were originally built inside Docker. Fix ownership before running any build commands:
38+
39+
```bash
40+
sudo chown -R $(whoami) public/assets
41+
```
42+
43+
**Build and watch** (recompiles automatically on save, Ctrl+C to stop):
44+
45+
```bash
46+
npm run build-dev
47+
```
48+
49+
To do a one-shot build without watch mode:
50+
51+
```bash
52+
./node_modules/.bin/webpack --config webpack.prod.js
53+
```
54+
55+
**Captcha:** The reset password and other pages require a Cloudflare Turnstile site key. Add test keys to `.env` to use the captcha locally without a real Cloudflare account:
56+
57+
```dotenv
58+
TURNSTILE_SITE_KEY=1x00000000000000000000AA
59+
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
60+
```
61+
62+
**Email:** Outbound email is logged to a date-stamped file in `storage/logs/` rather than sent. However, emails are queued via Redis by default, so they are not processed until a queue worker runs. After submitting the forgot password form, process the queue to trigger the log entry:
63+
64+
```bash
65+
docker exec idp-app php artisan queue:work --stop-when-empty
66+
```
67+
68+
This processes all pending jobs and exits when the queue is empty. Use `--once` if you only want to process a single job.
69+
70+
To retrieve a password reset link after processing the queue:
71+
72+
```bash
73+
grep "password/reset" storage/logs/laravel-$(date +%Y-%m-%d).log | tail -1
74+
```
75+
76+
After editing `.env`, clear the config cache:
77+
78+
```bash
79+
docker exec idp-app php artisan config:clear
80+
```
81+
3282
Redump the database
3383
===================
3484

config/auth.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,10 @@
103103
'password_min_length' => env('AUTH_PASSWORD_MIN_LENGTH', 8),
104104
'password_max_length' => env('AUTH_PASSWORD_MAX_LENGTH', 30),
105105
'password_allowed_special_characters' => env('AUTH_PASSWORD_ALLOWED_SPECIAL_CHARACTERS', '[A-Za-z0-9#?!@$%^&*+-]'),
106+
'password_allowed_special_characters_text' => env('AUTH_PASSWORD_ALLOWED_SPECIAL_CHARACTERS_TEXT', '#?!@$%^&*+-'),
106107
'password_shape_pattern' => env('AUTH_PASSWORD_SHAPE_PATTERN', '^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*+\-])[A-Za-z0-9#?!@$%^&*+\-]+$'),
107108
'password_shape_warning' => env('AUTH_PASSWORD_SHAPE_WARNING', 'Password must include at least one uppercase letter, one lowercase letter, one number, and one special character (#?!@$%^&*+-).'),
109+
'password_shape_list' => env('AUTH_PASSWORD_SHAPE_LIST', 'Uppercase letter,Lowercase letter,One number,Special character'),
108110
'verification_email_lifetime' => env("AUTH_VERIFICATION_EMAIL_LIFETIME", 600),
109111
'allows_native_auth' => env('AUTH_ALLOWS_NATIVE_AUTH', 1),
110112
'allows_native_on_config' => env('AUTH_ALLOWS_NATIVE_AUTH_CONFIG', 1),

resources/js/reset_password/reset_password.js

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import CardContent from "@material-ui/core/CardContent";
88
import Container from "@material-ui/core/Container";
99
import CssBaseline from "@material-ui/core/CssBaseline";
1010
import Grid from "@material-ui/core/Grid";
11+
import IconButton from "@material-ui/core/IconButton";
12+
import InputAdornment from "@material-ui/core/InputAdornment";
1113
import InfoOutlinedIcon from "@material-ui/icons/InfoOutlined";
14+
import Visibility from "@material-ui/icons/Visibility";
15+
import VisibilityOff from "@material-ui/icons/VisibilityOff";
16+
import CheckCircle from "@material-ui/icons/CheckCircle";
17+
import Cancel from "@material-ui/icons/Cancel";
18+
import RadioButtonUnchecked from "@material-ui/icons/RadioButtonUnchecked";
1219
import PasswordStrengthBar from "react-password-strength-bar";
1320
import TextField from "@material-ui/core/TextField";
1421
import Typography from "@material-ui/core/Typography";
@@ -39,6 +46,7 @@ const ResetPasswordPage = ({
3946
const formEl = useRef(null);
4047
const captcha = useRef(null);
4148
const [captchaConfirmation, setCaptchaConfirmation] = useState(null);
49+
const [showPassword, setShowPassword] = useState(false);
4250

4351
useEffect(() => {
4452
if (resetPasswordError) {
@@ -76,6 +84,8 @@ const ResetPasswordPage = ({
7684
}
7785
};
7886

87+
const passwordRequirementItems = passwordPolicy.shape_list.split(",").map(s => s.trim());
88+
7989
return (
8090
<Container component="main" maxWidth="xs" className={styles.main_container}>
8191
<CssBaseline />
@@ -94,8 +104,8 @@ const ResetPasswordPage = ({
94104
>
95105
<Card className={styles.reset_password_container} variant="outlined">
96106
<CardHeader
97-
title="Reset your Password"
98-
subheader="You can reset your password here."
107+
title="Set a new password."
108+
subheader="Resetting the password for the FNid below."
99109
/>
100110
<CardContent>
101111
<Grid
@@ -106,33 +116,48 @@ const ResetPasswordPage = ({
106116
>
107117
<Grid item>
108118
<TextField
119+
className={styles.email_field}
109120
id="email"
110121
name="email"
111122
autoComplete="email"
112-
variant="outlined"
123+
variant="filled"
113124
fullWidth
114125
size="small"
115-
label="Email Address"
126+
hiddenLabel
116127
value={formik.values.email}
117128
disabled={true}
129+
InputProps={{ disableUnderline: true }}
118130
/>
119131
</Grid>
120132
<Grid item xs={12}>
121133
<TextField
122134
id="password"
123135
name="password"
124-
type="password"
136+
type={showPassword ? "text" : "password"}
125137
variant="outlined"
126138
fullWidth
127139
size="small"
128-
label="Password"
140+
label="New password"
129141
inputProps={{maxLength: passwordPolicy.max_length}}
130142
value={formik.values.password}
131143
onChange={formik.handleChange}
132144
error={
133145
formik.touched.password && Boolean(formik.errors.password)
134146
}
135147
helperText={formik.touched.password && formik.errors.password}
148+
InputProps={{
149+
endAdornment: (
150+
<InputAdornment position="end">
151+
<IconButton
152+
onClick={() => setShowPassword(!showPassword)}
153+
edge="end"
154+
size="small"
155+
>
156+
{showPassword ? <VisibilityOff /> : <Visibility />}
157+
</IconButton>
158+
</InputAdornment>
159+
)
160+
}}
136161
/>
137162
{formik.values.password && (
138163
<PasswordStrengthBar
@@ -149,7 +174,7 @@ const ResetPasswordPage = ({
149174
variant="outlined"
150175
fullWidth
151176
size="small"
152-
label="Confirm Password"
177+
label="Confirm password"
153178
inputProps={{maxLength: passwordPolicy.max_length}}
154179
value={formik.values.password_confirmation}
155180
onChange={formik.handleChange}
@@ -161,14 +186,29 @@ const ResetPasswordPage = ({
161186
formik.touched.password_confirmation &&
162187
formik.errors.password_confirmation
163188
}
189+
InputProps={{
190+
endAdornment: (
191+
<InputAdornment position="end">
192+
{!formik.values.password_confirmation
193+
? <RadioButtonUnchecked style={{ color: '#ccc' }} />
194+
: formik.values.password_confirmation === formik.values.password && formik.values.password
195+
? <CheckCircle style={{ color: '#2e7d32' }} />
196+
: <Cancel style={{ color: '#c62828', opacity: 0.6 }} />
197+
}
198+
</InputAdornment>
199+
)
200+
}}
164201
/>
165202
</Grid>
166203
<Grid item className={styles.password_hint}>
167-
<InfoOutlinedIcon fontSize="small"/>
168-
&nbsp;
169-
<Typography variant="body2">
170-
<div dangerouslySetInnerHTML={{ __html: `The Password must be ${passwordPolicy.min_length}${passwordPolicy.max_length} characters, and ${passwordPolicy.shape_warning}` }} />
171-
</Typography>
204+
<p>Your password must include:</p>
205+
<ul>
206+
<li key="length">{passwordPolicy.min_length}{passwordPolicy.max_length} characters</li>
207+
{passwordRequirementItems.map((item, index) => (
208+
<li key={index}>{item}</li>
209+
))}
210+
</ul>
211+
<p className={styles.password_characters}>Allowed: <span dangerouslySetInnerHTML={{ __html: passwordPolicy.allowed_special_characters_text}} /></p>
172212
</Grid>
173213
<Grid item container alignItems="center" justifyContent="center">
174214
<Grid container item justify='center'>
@@ -195,6 +235,11 @@ const ResetPasswordPage = ({
195235
disableElevation
196236
fullWidth
197237
type="submit"
238+
disabled={
239+
!!formik.errors.password ||
240+
!formik.values.password_confirmation ||
241+
formik.values.password_confirmation !== formik.values.password
242+
}
198243
>
199244
{submitButtonText}
200245
</Button>
@@ -207,6 +252,7 @@ const ResetPasswordPage = ({
207252
<input type="hidden" value={initialValues.email} id="email" name="email"/>
208253
<input type="hidden" name="token" value={token}/>
209254
</form>
255+
<p className={styles.help_link}>Need help? <a href="mailto:support@fntech.com">support@fntech.com</a></p>
210256
</Container>
211257
);
212258
};

resources/js/reset_password/reset_password.module.scss

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
color: $text-color-dark;
2424
background-color: white;
2525

26+
.email_field {
27+
margin-bottom: 12px;
28+
}
29+
2630
.turnstile {
2731
margin-top: 10px;
2832
margin-bottom: 10px;
@@ -36,15 +40,48 @@
3640
}
3741

3842
.password_hint {
39-
display: flex;
4043
color: $hint-text-color;
41-
vertical-align: middle;
44+
background-color: #fafafa;
45+
margin: 8px 8px;
46+
}
47+
48+
.password_hint p:first-of-type {
49+
margin: 0 0 8px;
50+
}
51+
52+
.password_hint ul {
53+
list-style: none;
54+
margin-top: 8px;
55+
padding-left: 10px;
56+
display: grid;
57+
grid-template-columns: 1fr 1fr;
58+
gap: 4px 12px;
59+
}
60+
61+
.password_hint ul li {
62+
display: flex;
63+
align-items: center;
64+
gap: 60px;
65+
}
66+
67+
.password_characters {
68+
font-size: 0.85em;
69+
}
70+
71+
.password_characters span {
72+
letter-spacing: 0.5em;
4273
}
4374

4475
.error_label {
4576
color: #f44336;
4677
font-size: 0.75rem;
4778
margin: 4px 0 4px 14px;
4879
}
80+
81+
}
82+
83+
.help_link {
84+
margin: 1.5rem 0 0;
85+
text-align: center;
4986
}
5087
}

resources/views/auth/passwords/reset.blade.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
max_length: {{ Config::get("auth.password_max_length") }},
2525
shape_pattern: '{{ Config::get("auth.password_shape_pattern") }}',
2626
allowed_special_characters: '{{ Config::get("auth.password_allowed_special_characters") }}',
27-
shape_warning: '{{ Config::get("auth.password_shape_warning") }}'
27+
allowed_special_characters_text: '{{ Config::get("auth.password_allowed_special_characters_text") }}',
28+
shape_warning: '{{ Config::get("auth.password_shape_warning") }}',
29+
shape_list: '{{ Config::get("auth.password_shape_list") }}'
2830
}
2931
@if ($errors->any())
3032
@foreach($errors->all() as $error)
@@ -46,7 +48,7 @@
4648
resetPasswordError: error,
4749
sessionStatus: '{{ session("status") }}',
4850
showInfoBanner: parseInt('{{ Config::get("app.show_info_banner", 0) }}') === 1 ? true: false,
49-
submitButtonText: '{{ __("Password Reset") }}',
51+
submitButtonText: '{{ __("Set password") }}',
5052
}
5153
</script>
5254
{!! script_to('assets/resetPassword.js') !!}

0 commit comments

Comments
 (0)