hazo_auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/components.json +22 -0
- package/hazo_auth_config.example.ini +414 -0
- package/hazo_notify_config.example.ini +159 -0
- package/instrumentation.ts +32 -0
- package/migrations/001_add_token_type_to_refresh_tokens.sql +14 -0
- package/migrations/002_add_name_to_hazo_users.sql +7 -0
- package/next.config.mjs +55 -0
- package/package.json +114 -0
- package/postcss.config.mjs +8 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/apply_migration.ts +118 -0
- package/src/app/api/auth/change_password/route.ts +109 -0
- package/src/app/api/auth/forgot_password/route.ts +107 -0
- package/src/app/api/auth/library_photos/route.ts +70 -0
- package/src/app/api/auth/login/route.ts +155 -0
- package/src/app/api/auth/logout/route.ts +62 -0
- package/src/app/api/auth/me/route.ts +47 -0
- package/src/app/api/auth/profile_picture/[filename]/route.ts +67 -0
- package/src/app/api/auth/register/route.ts +106 -0
- package/src/app/api/auth/remove_profile_picture/route.ts +86 -0
- package/src/app/api/auth/resend_verification/route.ts +107 -0
- package/src/app/api/auth/reset_password/route.ts +107 -0
- package/src/app/api/auth/update_user/route.ts +126 -0
- package/src/app/api/auth/upload_profile_picture/route.ts +268 -0
- package/src/app/api/auth/validate_reset_token/route.ts +80 -0
- package/src/app/api/auth/verify_email/route.ts +85 -0
- package/src/app/api/migrations/apply/route.ts +91 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/fonts/GeistMonoVF.woff +0 -0
- package/src/app/fonts/GeistVF.woff +0 -0
- package/src/app/forgot_password/forgot_password_page_client.tsx +60 -0
- package/src/app/forgot_password/page.tsx +24 -0
- package/src/app/globals.css +89 -0
- package/src/app/hazo_connect/api/sqlite/data/route.ts +197 -0
- package/src/app/hazo_connect/api/sqlite/schema/route.ts +35 -0
- package/src/app/hazo_connect/api/sqlite/tables/route.ts +26 -0
- package/src/app/hazo_connect/sqlite_admin/page.tsx +51 -0
- package/src/app/hazo_connect/sqlite_admin/sqlite-admin-client.tsx +947 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/login/login_page_client.tsx +71 -0
- package/src/app/login/page.tsx +26 -0
- package/src/app/my_settings/my_settings_page_client.tsx +120 -0
- package/src/app/my_settings/page.tsx +40 -0
- package/src/app/page.tsx +170 -0
- package/src/app/register/page.tsx +26 -0
- package/src/app/register/register_page_client.tsx +72 -0
- package/src/app/reset_password/page.tsx +29 -0
- package/src/app/reset_password/reset_password_page_client.tsx +81 -0
- package/src/app/verify_email/page.tsx +24 -0
- package/src/app/verify_email/verify_email_page_client.tsx +60 -0
- package/src/components/layouts/email_verification/config/email_verification_field_config.ts +86 -0
- package/src/components/layouts/email_verification/hooks/use_email_verification.ts +291 -0
- package/src/components/layouts/email_verification/index.tsx +297 -0
- package/src/components/layouts/forgot_password/config/forgot_password_field_config.ts +58 -0
- package/src/components/layouts/forgot_password/hooks/use_forgot_password_form.ts +179 -0
- package/src/components/layouts/forgot_password/index.tsx +168 -0
- package/src/components/layouts/login/config/login_field_config.ts +67 -0
- package/src/components/layouts/login/hooks/use_login_form.ts +281 -0
- package/src/components/layouts/login/index.tsx +224 -0
- package/src/components/layouts/my_settings/components/editable_field.tsx +177 -0
- package/src/components/layouts/my_settings/components/password_change_dialog.tsx +301 -0
- package/src/components/layouts/my_settings/components/profile_picture_dialog.tsx +385 -0
- package/src/components/layouts/my_settings/components/profile_picture_display.tsx +66 -0
- package/src/components/layouts/my_settings/components/profile_picture_gravatar_tab.tsx +143 -0
- package/src/components/layouts/my_settings/components/profile_picture_library_tab.tsx +282 -0
- package/src/components/layouts/my_settings/components/profile_picture_upload_tab.tsx +341 -0
- package/src/components/layouts/my_settings/config/my_settings_field_config.ts +61 -0
- package/src/components/layouts/my_settings/hooks/use_my_settings.ts +458 -0
- package/src/components/layouts/my_settings/index.tsx +351 -0
- package/src/components/layouts/register/config/register_field_config.ts +101 -0
- package/src/components/layouts/register/hooks/use_register_form.ts +272 -0
- package/src/components/layouts/register/index.tsx +208 -0
- package/src/components/layouts/reset_password/config/reset_password_field_config.ts +86 -0
- package/src/components/layouts/reset_password/hooks/use_reset_password_form.ts +276 -0
- package/src/components/layouts/reset_password/index.tsx +294 -0
- package/src/components/layouts/shared/components/already_logged_in_guard.tsx +95 -0
- package/src/components/layouts/shared/components/field_error_message.tsx +29 -0
- package/src/components/layouts/shared/components/form_action_buttons.tsx +64 -0
- package/src/components/layouts/shared/components/form_field_wrapper.tsx +44 -0
- package/src/components/layouts/shared/components/form_header.tsx +36 -0
- package/src/components/layouts/shared/components/logout_button.tsx +76 -0
- package/src/components/layouts/shared/components/password_field.tsx +72 -0
- package/src/components/layouts/shared/components/sidebar_layout_wrapper.tsx +264 -0
- package/src/components/layouts/shared/components/two_column_auth_layout.tsx +44 -0
- package/src/components/layouts/shared/components/unauthorized_guard.tsx +78 -0
- package/src/components/layouts/shared/components/visual_panel.tsx +41 -0
- package/src/components/layouts/shared/config/layout_customization.ts +95 -0
- package/src/components/layouts/shared/data/layout_data_client.ts +19 -0
- package/src/components/layouts/shared/hooks/use_auth_status.ts +103 -0
- package/src/components/layouts/shared/utils/ip_address.ts +37 -0
- package/src/components/layouts/shared/utils/validation.ts +66 -0
- package/src/components/ui/avatar.tsx +50 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/hazo_ui_tooltip.tsx +67 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/sheet.tsx +139 -0
- package/src/components/ui/sidebar.tsx +773 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +31 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/ui/vertical-tabs.tsx +59 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/lib/already_logged_in_config.server.ts +46 -0
- package/src/lib/app_logger.ts +24 -0
- package/src/lib/auth/auth_utils.server.ts +196 -0
- package/src/lib/auth/server_auth.ts +88 -0
- package/src/lib/config/config_loader.server.ts +149 -0
- package/src/lib/email_verification_config.server.ts +32 -0
- package/src/lib/file_types_config.server.ts +25 -0
- package/src/lib/forgot_password_config.server.ts +32 -0
- package/src/lib/hazo_connect_instance.server.ts +77 -0
- package/src/lib/hazo_connect_setup.server.ts +181 -0
- package/src/lib/hazo_connect_setup.ts +54 -0
- package/src/lib/login_config.server.ts +46 -0
- package/src/lib/messages_config.server.ts +45 -0
- package/src/lib/migrations/apply_migration.ts +105 -0
- package/src/lib/my_settings_config.server.ts +135 -0
- package/src/lib/password_requirements_config.server.ts +39 -0
- package/src/lib/profile_picture_config.server.ts +56 -0
- package/src/lib/register_config.server.ts +57 -0
- package/src/lib/reset_password_config.server.ts +75 -0
- package/src/lib/services/email_service.ts +581 -0
- package/src/lib/services/email_verification_service.ts +264 -0
- package/src/lib/services/login_service.ts +118 -0
- package/src/lib/services/password_change_service.ts +154 -0
- package/src/lib/services/password_reset_service.ts +405 -0
- package/src/lib/services/profile_picture_remove_service.ts +120 -0
- package/src/lib/services/profile_picture_service.ts +215 -0
- package/src/lib/services/profile_picture_source_mapper.ts +62 -0
- package/src/lib/services/registration_service.ts +163 -0
- package/src/lib/services/token_service.ts +240 -0
- package/src/lib/services/user_update_service.ts +128 -0
- package/src/lib/ui_sizes_config.server.ts +37 -0
- package/src/lib/user_fields_config.server.ts +31 -0
- package/src/lib/utils/api_route_helpers.ts +60 -0
- package/src/lib/utils.ts +11 -0
- package/src/middleware.ts +91 -0
- package/src/server/config/config_loader.ts +496 -0
- package/src/server/index.ts +38 -0
- package/src/server/logging/logger_service.ts +56 -0
- package/src/server/routes/root_router.ts +16 -0
- package/src/server/server.ts +28 -0
- package/src/server/types/app_types.ts +74 -0
- package/src/server/types/express.d.ts +15 -0
- package/src/stories/email_verification_layout.stories.tsx +137 -0
- package/src/stories/forgot_password_layout.stories.tsx +85 -0
- package/src/stories/login_layout.stories.tsx +85 -0
- package/src/stories/project_overview.stories.tsx +33 -0
- package/src/stories/register_layout.stories.tsx +107 -0
- package/tailwind.config.ts +77 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// file_description: encapsulate register form state, validation, and data interactions
|
|
2
|
+
// section: imports
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import type { LayoutDataClient } from "@/components/layouts/shared/data/layout_data_client";
|
|
6
|
+
import type { PasswordRequirementOptions, PasswordRequirementOverrides } from "@/components/layouts/shared/config/layout_customization";
|
|
7
|
+
import { REGISTER_FIELD_IDS, type RegisterFieldId } from "@/components/layouts/register/config/register_field_config";
|
|
8
|
+
import { validateEmail, validatePassword } from "@/components/layouts/shared/utils/validation";
|
|
9
|
+
|
|
10
|
+
// section: constants
|
|
11
|
+
const PASSWORD_FIELDS: Array<RegisterFieldId> = [
|
|
12
|
+
REGISTER_FIELD_IDS.PASSWORD,
|
|
13
|
+
REGISTER_FIELD_IDS.CONFIRM_PASSWORD,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// section: types
|
|
17
|
+
export type RegisterFormValues = Record<RegisterFieldId, string>;
|
|
18
|
+
export type RegisterFormErrors = Partial<Record<RegisterFieldId, string | string[]>> & {
|
|
19
|
+
submit?: string;
|
|
20
|
+
};
|
|
21
|
+
export type PasswordVisibilityState = Record<
|
|
22
|
+
Extract<RegisterFieldId, "password" | "confirm_password">,
|
|
23
|
+
boolean
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
export type UseRegisterFormParams<TClient = unknown> = {
|
|
27
|
+
showNameField: boolean;
|
|
28
|
+
passwordRequirements: PasswordRequirementOptions;
|
|
29
|
+
passwordRequirementOverrides?: PasswordRequirementOverrides;
|
|
30
|
+
dataClient: LayoutDataClient<TClient>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type UseRegisterFormResult = {
|
|
34
|
+
values: RegisterFormValues;
|
|
35
|
+
errors: RegisterFormErrors;
|
|
36
|
+
passwordVisibility: PasswordVisibilityState;
|
|
37
|
+
isSubmitDisabled: boolean;
|
|
38
|
+
isSubmitting: boolean;
|
|
39
|
+
emailTouched: boolean;
|
|
40
|
+
handleFieldChange: (fieldId: RegisterFieldId, value: string) => void;
|
|
41
|
+
handleEmailBlur: () => void;
|
|
42
|
+
togglePasswordVisibility: (fieldId: "password" | "confirm_password") => void;
|
|
43
|
+
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
|
44
|
+
handleCancel: () => void;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// section: helpers
|
|
48
|
+
const buildInitialValues = (): RegisterFormValues => ({
|
|
49
|
+
[REGISTER_FIELD_IDS.NAME]: "",
|
|
50
|
+
[REGISTER_FIELD_IDS.EMAIL]: "",
|
|
51
|
+
[REGISTER_FIELD_IDS.PASSWORD]: "",
|
|
52
|
+
[REGISTER_FIELD_IDS.CONFIRM_PASSWORD]: "",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// section: hook
|
|
57
|
+
export const use_register_form = <TClient,>({
|
|
58
|
+
showNameField,
|
|
59
|
+
passwordRequirements,
|
|
60
|
+
dataClient,
|
|
61
|
+
}: UseRegisterFormParams<TClient>): UseRegisterFormResult => {
|
|
62
|
+
const [values, setValues] = useState<RegisterFormValues>(buildInitialValues);
|
|
63
|
+
const [errors, setErrors] = useState<RegisterFormErrors>({});
|
|
64
|
+
const [passwordVisibility, setPasswordVisibility] = useState<PasswordVisibilityState>({
|
|
65
|
+
password: false,
|
|
66
|
+
confirm_password: false,
|
|
67
|
+
});
|
|
68
|
+
const [emailTouched, setEmailTouched] = useState<boolean>(false);
|
|
69
|
+
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
70
|
+
|
|
71
|
+
const isSubmitDisabled = useMemo(() => {
|
|
72
|
+
if (isSubmitting) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const hasEmptyField = Object.entries(values).some(([fieldId, fieldValue]) => {
|
|
77
|
+
if (fieldId === REGISTER_FIELD_IDS.NAME && !showNameField) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return fieldValue.trim() === "";
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const hasErrors = Object.keys(errors).length > 0;
|
|
84
|
+
return hasEmptyField || hasErrors;
|
|
85
|
+
}, [errors, showNameField, values, isSubmitting]);
|
|
86
|
+
|
|
87
|
+
const togglePasswordVisibility = useCallback((fieldId: "password" | "confirm_password") => {
|
|
88
|
+
setPasswordVisibility((previous) => ({
|
|
89
|
+
...previous,
|
|
90
|
+
[fieldId]: !previous[fieldId],
|
|
91
|
+
}));
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const handleFieldChange = useCallback(
|
|
95
|
+
(fieldId: RegisterFieldId, value: string) => {
|
|
96
|
+
setValues((previousValues) => {
|
|
97
|
+
const nextValues: RegisterFormValues = {
|
|
98
|
+
...previousValues,
|
|
99
|
+
[fieldId]: value,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
setErrors((previousErrors) => {
|
|
103
|
+
const updatedErrors: RegisterFormErrors = { ...previousErrors };
|
|
104
|
+
|
|
105
|
+
// Only validate email on change if it has been touched (blurred)
|
|
106
|
+
if (fieldId === REGISTER_FIELD_IDS.EMAIL && emailTouched) {
|
|
107
|
+
const emailError = validateEmail(value);
|
|
108
|
+
if (emailError) {
|
|
109
|
+
updatedErrors[REGISTER_FIELD_IDS.EMAIL] = emailError;
|
|
110
|
+
} else {
|
|
111
|
+
delete updatedErrors[REGISTER_FIELD_IDS.EMAIL];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (PASSWORD_FIELDS.includes(fieldId)) {
|
|
116
|
+
const passwordError = validatePassword(
|
|
117
|
+
nextValues[REGISTER_FIELD_IDS.PASSWORD],
|
|
118
|
+
passwordRequirements,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (passwordError) {
|
|
122
|
+
updatedErrors[REGISTER_FIELD_IDS.PASSWORD] = passwordError;
|
|
123
|
+
} else {
|
|
124
|
+
delete updatedErrors[REGISTER_FIELD_IDS.PASSWORD];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
nextValues[REGISTER_FIELD_IDS.CONFIRM_PASSWORD].trim().length > 0 &&
|
|
129
|
+
nextValues[REGISTER_FIELD_IDS.PASSWORD] !==
|
|
130
|
+
nextValues[REGISTER_FIELD_IDS.CONFIRM_PASSWORD]
|
|
131
|
+
) {
|
|
132
|
+
updatedErrors[REGISTER_FIELD_IDS.CONFIRM_PASSWORD] = "passwords do not match";
|
|
133
|
+
} else {
|
|
134
|
+
delete updatedErrors[REGISTER_FIELD_IDS.CONFIRM_PASSWORD];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return updatedErrors;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return nextValues;
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
[passwordRequirements, emailTouched],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const handleEmailBlur = useCallback(() => {
|
|
148
|
+
setEmailTouched(true);
|
|
149
|
+
// Validate email on blur
|
|
150
|
+
setErrors((previousErrors) => {
|
|
151
|
+
const updatedErrors: RegisterFormErrors = { ...previousErrors };
|
|
152
|
+
const emailValue = values[REGISTER_FIELD_IDS.EMAIL];
|
|
153
|
+
const emailError = validateEmail(emailValue);
|
|
154
|
+
if (emailError) {
|
|
155
|
+
updatedErrors[REGISTER_FIELD_IDS.EMAIL] = emailError;
|
|
156
|
+
} else {
|
|
157
|
+
delete updatedErrors[REGISTER_FIELD_IDS.EMAIL];
|
|
158
|
+
}
|
|
159
|
+
return updatedErrors;
|
|
160
|
+
});
|
|
161
|
+
}, [values]);
|
|
162
|
+
|
|
163
|
+
const handleSubmit = useCallback(
|
|
164
|
+
async (event: React.FormEvent<HTMLFormElement>) => {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
|
|
167
|
+
// Final validation
|
|
168
|
+
const emailError = validateEmail(values[REGISTER_FIELD_IDS.EMAIL]);
|
|
169
|
+
const passwordError = validatePassword(
|
|
170
|
+
values[REGISTER_FIELD_IDS.PASSWORD],
|
|
171
|
+
passwordRequirements,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (emailError || passwordError) {
|
|
175
|
+
setErrors({
|
|
176
|
+
...(emailError ? { [REGISTER_FIELD_IDS.EMAIL]: emailError } : {}),
|
|
177
|
+
...(passwordError ? { [REGISTER_FIELD_IDS.PASSWORD]: passwordError } : {}),
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check password match
|
|
183
|
+
if (
|
|
184
|
+
values[REGISTER_FIELD_IDS.PASSWORD] !==
|
|
185
|
+
values[REGISTER_FIELD_IDS.CONFIRM_PASSWORD]
|
|
186
|
+
) {
|
|
187
|
+
setErrors({
|
|
188
|
+
[REGISTER_FIELD_IDS.CONFIRM_PASSWORD]: "passwords do not match",
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setIsSubmitting(true);
|
|
194
|
+
setErrors({});
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const response = await fetch("/api/auth/register", {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
},
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
name: values[REGISTER_FIELD_IDS.NAME] || undefined,
|
|
204
|
+
email: values[REGISTER_FIELD_IDS.EMAIL],
|
|
205
|
+
password: values[REGISTER_FIELD_IDS.PASSWORD],
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const data = await response.json();
|
|
210
|
+
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
throw new Error(data.error || "Registration failed");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Show success notification
|
|
216
|
+
toast.success("Registration successful!", {
|
|
217
|
+
description: "Your account has been created successfully.",
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Reset form on success
|
|
221
|
+
setValues(buildInitialValues());
|
|
222
|
+
setErrors({});
|
|
223
|
+
setPasswordVisibility({
|
|
224
|
+
password: false,
|
|
225
|
+
confirm_password: false,
|
|
226
|
+
});
|
|
227
|
+
setEmailTouched(false);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const errorMessage =
|
|
230
|
+
error instanceof Error ? error.message : "Registration failed. Please try again.";
|
|
231
|
+
|
|
232
|
+
// Show error notification
|
|
233
|
+
toast.error("Registration failed", {
|
|
234
|
+
description: errorMessage,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Set error state
|
|
238
|
+
setErrors({
|
|
239
|
+
submit: errorMessage,
|
|
240
|
+
});
|
|
241
|
+
} finally {
|
|
242
|
+
setIsSubmitting(false);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
[values, passwordRequirements, dataClient],
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const handleCancel = useCallback(() => {
|
|
249
|
+
setValues(buildInitialValues());
|
|
250
|
+
setErrors({});
|
|
251
|
+
setPasswordVisibility({
|
|
252
|
+
password: false,
|
|
253
|
+
confirm_password: false,
|
|
254
|
+
});
|
|
255
|
+
setEmailTouched(false);
|
|
256
|
+
}, []);
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
values,
|
|
260
|
+
errors,
|
|
261
|
+
passwordVisibility,
|
|
262
|
+
isSubmitDisabled,
|
|
263
|
+
isSubmitting,
|
|
264
|
+
emailTouched,
|
|
265
|
+
handleFieldChange,
|
|
266
|
+
handleEmailBlur,
|
|
267
|
+
togglePasswordVisibility,
|
|
268
|
+
handleSubmit,
|
|
269
|
+
handleCancel,
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// file_description: register layout component built atop shared layout utilities
|
|
2
|
+
// section: client_directive
|
|
3
|
+
"use client";
|
|
4
|
+
|
|
5
|
+
// section: imports
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import { PasswordField } from "@/components/layouts/shared/components/password_field";
|
|
8
|
+
import { FormFieldWrapper } from "@/components/layouts/shared/components/form_field_wrapper";
|
|
9
|
+
import { FormHeader } from "@/components/layouts/shared/components/form_header";
|
|
10
|
+
import { FormActionButtons } from "@/components/layouts/shared/components/form_action_buttons";
|
|
11
|
+
import { TwoColumnAuthLayout } from "@/components/layouts/shared/components/two_column_auth_layout";
|
|
12
|
+
import { AlreadyLoggedInGuard } from "@/components/layouts/shared/components/already_logged_in_guard";
|
|
13
|
+
import {
|
|
14
|
+
type ButtonPaletteOverrides,
|
|
15
|
+
type LayoutFieldMapOverrides,
|
|
16
|
+
type LayoutLabelOverrides,
|
|
17
|
+
type PasswordRequirementOverrides,
|
|
18
|
+
} from "@/components/layouts/shared/config/layout_customization";
|
|
19
|
+
import {
|
|
20
|
+
REGISTER_FIELD_IDS,
|
|
21
|
+
createRegisterFieldDefinitions,
|
|
22
|
+
resolveRegisterButtonPalette,
|
|
23
|
+
resolveRegisterLabels,
|
|
24
|
+
resolveRegisterPasswordRequirements,
|
|
25
|
+
} from "@/components/layouts/register/config/register_field_config";
|
|
26
|
+
import {
|
|
27
|
+
use_register_form,
|
|
28
|
+
type UseRegisterFormResult,
|
|
29
|
+
} from "@/components/layouts/register/hooks/use_register_form";
|
|
30
|
+
import { type LayoutDataClient } from "@/components/layouts/shared/data/layout_data_client";
|
|
31
|
+
|
|
32
|
+
// section: types
|
|
33
|
+
export type RegisterLayoutProps<TClient = unknown> = {
|
|
34
|
+
image_src: string;
|
|
35
|
+
image_alt: string;
|
|
36
|
+
image_background_color?: string;
|
|
37
|
+
field_overrides?: LayoutFieldMapOverrides;
|
|
38
|
+
labels?: LayoutLabelOverrides;
|
|
39
|
+
button_colors?: ButtonPaletteOverrides;
|
|
40
|
+
password_requirements?: PasswordRequirementOverrides;
|
|
41
|
+
show_name_field?: boolean;
|
|
42
|
+
data_client: LayoutDataClient<TClient>;
|
|
43
|
+
alreadyLoggedInMessage?: string;
|
|
44
|
+
showLogoutButton?: boolean;
|
|
45
|
+
showReturnHomeButton?: boolean;
|
|
46
|
+
returnHomeButtonLabel?: string;
|
|
47
|
+
returnHomePath?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ORDERED_FIELDS: RegisterFieldId[] = [
|
|
51
|
+
REGISTER_FIELD_IDS.NAME,
|
|
52
|
+
REGISTER_FIELD_IDS.EMAIL,
|
|
53
|
+
REGISTER_FIELD_IDS.PASSWORD,
|
|
54
|
+
REGISTER_FIELD_IDS.CONFIRM_PASSWORD,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
type RegisterFieldId = (typeof REGISTER_FIELD_IDS)[keyof typeof REGISTER_FIELD_IDS];
|
|
58
|
+
|
|
59
|
+
// section: component
|
|
60
|
+
export default function register_layout<TClient>({
|
|
61
|
+
image_src,
|
|
62
|
+
image_alt,
|
|
63
|
+
image_background_color = "#f1f5f9",
|
|
64
|
+
field_overrides,
|
|
65
|
+
labels,
|
|
66
|
+
button_colors,
|
|
67
|
+
password_requirements,
|
|
68
|
+
show_name_field = true,
|
|
69
|
+
data_client,
|
|
70
|
+
alreadyLoggedInMessage = "You are already logged in",
|
|
71
|
+
showLogoutButton = true,
|
|
72
|
+
showReturnHomeButton = false,
|
|
73
|
+
returnHomeButtonLabel = "Return home",
|
|
74
|
+
returnHomePath = "/",
|
|
75
|
+
}: RegisterLayoutProps<TClient>) {
|
|
76
|
+
const fieldDefinitions = createRegisterFieldDefinitions(field_overrides);
|
|
77
|
+
const resolvedLabels = resolveRegisterLabels(labels);
|
|
78
|
+
const resolvedButtonPalette = resolveRegisterButtonPalette(button_colors);
|
|
79
|
+
const resolvedPasswordRequirements = resolveRegisterPasswordRequirements(
|
|
80
|
+
password_requirements,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const form = use_register_form({
|
|
84
|
+
showNameField: show_name_field,
|
|
85
|
+
passwordRequirements: resolvedPasswordRequirements,
|
|
86
|
+
dataClient: data_client,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const renderFields = (formState: UseRegisterFormResult) => {
|
|
90
|
+
const renderOrder = ORDERED_FIELDS.filter(
|
|
91
|
+
(fieldId) => show_name_field || fieldId !== REGISTER_FIELD_IDS.NAME,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return renderOrder.map((fieldId) => {
|
|
95
|
+
const fieldDefinition = fieldDefinitions[fieldId];
|
|
96
|
+
const fieldValue = formState.values[fieldId];
|
|
97
|
+
const fieldError = formState.errors[fieldId];
|
|
98
|
+
|
|
99
|
+
const isPasswordField =
|
|
100
|
+
fieldDefinition.type === "password" &&
|
|
101
|
+
(fieldId === REGISTER_FIELD_IDS.PASSWORD ||
|
|
102
|
+
fieldId === REGISTER_FIELD_IDS.CONFIRM_PASSWORD);
|
|
103
|
+
|
|
104
|
+
const inputElement = isPasswordField ? (
|
|
105
|
+
<PasswordField
|
|
106
|
+
inputId={fieldDefinition.id}
|
|
107
|
+
ariaLabel={fieldDefinition.ariaLabel}
|
|
108
|
+
value={fieldValue}
|
|
109
|
+
placeholder={fieldDefinition.placeholder}
|
|
110
|
+
autoComplete={fieldDefinition.autoComplete}
|
|
111
|
+
isVisible={formState.passwordVisibility[fieldDefinition.id as "password" | "confirm_password"]}
|
|
112
|
+
onChange={(nextValue) => formState.handleFieldChange(fieldId, nextValue)}
|
|
113
|
+
onToggleVisibility={() =>
|
|
114
|
+
formState.togglePasswordVisibility(fieldDefinition.id as "password" | "confirm_password")
|
|
115
|
+
}
|
|
116
|
+
errorMessage={fieldError as string | string[] | undefined}
|
|
117
|
+
/>
|
|
118
|
+
) : (
|
|
119
|
+
<Input
|
|
120
|
+
id={fieldDefinition.id}
|
|
121
|
+
type={fieldDefinition.type}
|
|
122
|
+
value={fieldValue}
|
|
123
|
+
onChange={(event) =>
|
|
124
|
+
formState.handleFieldChange(fieldId, event.target.value)
|
|
125
|
+
}
|
|
126
|
+
onBlur={
|
|
127
|
+
fieldId === REGISTER_FIELD_IDS.EMAIL
|
|
128
|
+
? formState.handleEmailBlur
|
|
129
|
+
: undefined
|
|
130
|
+
}
|
|
131
|
+
autoComplete={fieldDefinition.autoComplete}
|
|
132
|
+
placeholder={fieldDefinition.placeholder}
|
|
133
|
+
aria-label={fieldDefinition.ariaLabel}
|
|
134
|
+
className="cls_register_layout_field_input"
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Only show email error if field has been touched (blurred)
|
|
139
|
+
const shouldShowError =
|
|
140
|
+
isPasswordField
|
|
141
|
+
? undefined
|
|
142
|
+
: fieldId === REGISTER_FIELD_IDS.EMAIL
|
|
143
|
+
? formState.emailTouched && fieldError
|
|
144
|
+
? fieldError
|
|
145
|
+
: undefined
|
|
146
|
+
: fieldError;
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<FormFieldWrapper
|
|
150
|
+
key={fieldId}
|
|
151
|
+
fieldId={fieldDefinition.id}
|
|
152
|
+
label={fieldDefinition.label}
|
|
153
|
+
input={inputElement}
|
|
154
|
+
errorMessage={shouldShowError}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<AlreadyLoggedInGuard
|
|
162
|
+
image_src={image_src}
|
|
163
|
+
image_alt={image_alt}
|
|
164
|
+
image_background_color={image_background_color}
|
|
165
|
+
message={alreadyLoggedInMessage}
|
|
166
|
+
showLogoutButton={showLogoutButton}
|
|
167
|
+
showReturnHomeButton={showReturnHomeButton}
|
|
168
|
+
returnHomeButtonLabel={returnHomeButtonLabel}
|
|
169
|
+
returnHomePath={returnHomePath}
|
|
170
|
+
>
|
|
171
|
+
<TwoColumnAuthLayout
|
|
172
|
+
imageSrc={image_src}
|
|
173
|
+
imageAlt={image_alt}
|
|
174
|
+
imageBackgroundColor={image_background_color}
|
|
175
|
+
formContent={
|
|
176
|
+
<>
|
|
177
|
+
<FormHeader
|
|
178
|
+
heading={resolvedLabels.heading}
|
|
179
|
+
subHeading={resolvedLabels.subHeading}
|
|
180
|
+
/>
|
|
181
|
+
<form
|
|
182
|
+
className="cls_register_layout_form_fields flex flex-col gap-5"
|
|
183
|
+
onSubmit={form.handleSubmit}
|
|
184
|
+
aria-label="Registration form"
|
|
185
|
+
>
|
|
186
|
+
{renderFields(form)}
|
|
187
|
+
<FormActionButtons
|
|
188
|
+
submitLabel={resolvedLabels.submitButton}
|
|
189
|
+
cancelLabel={resolvedLabels.cancelButton}
|
|
190
|
+
buttonPalette={resolvedButtonPalette}
|
|
191
|
+
isSubmitDisabled={form.isSubmitDisabled}
|
|
192
|
+
onCancel={form.handleCancel}
|
|
193
|
+
submitAriaLabel="Submit registration form"
|
|
194
|
+
cancelAriaLabel="Cancel registration form"
|
|
195
|
+
/>
|
|
196
|
+
{form.isSubmitting && (
|
|
197
|
+
<div className="cls_register_submitting_indicator text-sm text-slate-600 text-center">
|
|
198
|
+
Registering...
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</form>
|
|
202
|
+
</>
|
|
203
|
+
}
|
|
204
|
+
/>
|
|
205
|
+
</AlreadyLoggedInGuard>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// file_description: reset password layout specific configuration helpers
|
|
2
|
+
// section: imports
|
|
3
|
+
import type { LayoutFieldMap, LayoutFieldMapOverrides } from "@/components/layouts/shared/config/layout_customization";
|
|
4
|
+
import {
|
|
5
|
+
resolveButtonPalette,
|
|
6
|
+
resolveFieldDefinitions,
|
|
7
|
+
resolveLabels,
|
|
8
|
+
type ButtonPaletteDefaults,
|
|
9
|
+
type ButtonPaletteOverrides,
|
|
10
|
+
type LayoutLabelDefaults,
|
|
11
|
+
type LayoutLabelOverrides,
|
|
12
|
+
type PasswordRequirementOptions,
|
|
13
|
+
type PasswordRequirementOverrides,
|
|
14
|
+
resolvePasswordRequirements,
|
|
15
|
+
} from "@/components/layouts/shared/config/layout_customization";
|
|
16
|
+
|
|
17
|
+
// section: field_identifiers
|
|
18
|
+
export const RESET_PASSWORD_FIELD_IDS = {
|
|
19
|
+
PASSWORD: "password",
|
|
20
|
+
CONFIRM_PASSWORD: "confirm_password",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export type ResetPasswordFieldId = (typeof RESET_PASSWORD_FIELD_IDS)[keyof typeof RESET_PASSWORD_FIELD_IDS];
|
|
24
|
+
|
|
25
|
+
// section: field_definitions
|
|
26
|
+
const RESET_PASSWORD_FIELD_DEFINITIONS: LayoutFieldMap = {
|
|
27
|
+
[RESET_PASSWORD_FIELD_IDS.PASSWORD]: {
|
|
28
|
+
id: RESET_PASSWORD_FIELD_IDS.PASSWORD,
|
|
29
|
+
label: "New password",
|
|
30
|
+
type: "password",
|
|
31
|
+
autoComplete: "new-password",
|
|
32
|
+
placeholder: "Enter your new password",
|
|
33
|
+
ariaLabel: "New password input field",
|
|
34
|
+
},
|
|
35
|
+
[RESET_PASSWORD_FIELD_IDS.CONFIRM_PASSWORD]: {
|
|
36
|
+
id: RESET_PASSWORD_FIELD_IDS.CONFIRM_PASSWORD,
|
|
37
|
+
label: "Confirm new password",
|
|
38
|
+
type: "password",
|
|
39
|
+
autoComplete: "new-password",
|
|
40
|
+
placeholder: "Re-enter your new password",
|
|
41
|
+
ariaLabel: "Confirm new password input field",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const createResetPasswordFieldDefinitions = (
|
|
46
|
+
overrides?: LayoutFieldMapOverrides,
|
|
47
|
+
) => resolveFieldDefinitions(RESET_PASSWORD_FIELD_DEFINITIONS, overrides);
|
|
48
|
+
|
|
49
|
+
// section: label_defaults
|
|
50
|
+
const RESET_PASSWORD_LABEL_DEFAULTS: LayoutLabelDefaults = {
|
|
51
|
+
heading: "Reset your password",
|
|
52
|
+
subHeading: "Enter your new password below.",
|
|
53
|
+
submitButton: "Reset password",
|
|
54
|
+
cancelButton: "Cancel",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const resolveResetPasswordLabels = (overrides?: LayoutLabelOverrides) =>
|
|
58
|
+
resolveLabels(RESET_PASSWORD_LABEL_DEFAULTS, overrides);
|
|
59
|
+
|
|
60
|
+
// section: button_palette_defaults
|
|
61
|
+
const RESET_PASSWORD_BUTTON_PALETTE_DEFAULTS: ButtonPaletteDefaults = {
|
|
62
|
+
submitBackground: "#0f172a",
|
|
63
|
+
submitText: "#ffffff",
|
|
64
|
+
cancelBorder: "#cbd5f5",
|
|
65
|
+
cancelText: "#0f172a",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const resolveResetPasswordButtonPalette = (overrides?: ButtonPaletteOverrides) =>
|
|
69
|
+
resolveButtonPalette(RESET_PASSWORD_BUTTON_PALETTE_DEFAULTS, overrides);
|
|
70
|
+
|
|
71
|
+
// section: password_requirements_defaults
|
|
72
|
+
const RESET_PASSWORD_PASSWORD_REQUIREMENT_DEFAULTS: PasswordRequirementOptions = {
|
|
73
|
+
minimum_length: 8,
|
|
74
|
+
require_uppercase: false,
|
|
75
|
+
require_lowercase: false,
|
|
76
|
+
require_number: false,
|
|
77
|
+
require_special: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const resolveResetPasswordPasswordRequirements = (
|
|
81
|
+
overrides?: PasswordRequirementOverrides,
|
|
82
|
+
) => resolvePasswordRequirements(RESET_PASSWORD_PASSWORD_REQUIREMENT_DEFAULTS, overrides);
|
|
83
|
+
|
|
84
|
+
// section: already_logged_in_label
|
|
85
|
+
export const RESET_PASSWORD_ALREADY_LOGGED_IN_MESSAGE_DEFAULT = "You're already logged in.";
|
|
86
|
+
|