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.
Files changed (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/components.json +22 -0
  4. package/hazo_auth_config.example.ini +414 -0
  5. package/hazo_notify_config.example.ini +159 -0
  6. package/instrumentation.ts +32 -0
  7. package/migrations/001_add_token_type_to_refresh_tokens.sql +14 -0
  8. package/migrations/002_add_name_to_hazo_users.sql +7 -0
  9. package/next.config.mjs +55 -0
  10. package/package.json +114 -0
  11. package/postcss.config.mjs +8 -0
  12. package/public/file.svg +1 -0
  13. package/public/globe.svg +1 -0
  14. package/public/next.svg +1 -0
  15. package/public/vercel.svg +1 -0
  16. package/public/window.svg +1 -0
  17. package/scripts/apply_migration.ts +118 -0
  18. package/src/app/api/auth/change_password/route.ts +109 -0
  19. package/src/app/api/auth/forgot_password/route.ts +107 -0
  20. package/src/app/api/auth/library_photos/route.ts +70 -0
  21. package/src/app/api/auth/login/route.ts +155 -0
  22. package/src/app/api/auth/logout/route.ts +62 -0
  23. package/src/app/api/auth/me/route.ts +47 -0
  24. package/src/app/api/auth/profile_picture/[filename]/route.ts +67 -0
  25. package/src/app/api/auth/register/route.ts +106 -0
  26. package/src/app/api/auth/remove_profile_picture/route.ts +86 -0
  27. package/src/app/api/auth/resend_verification/route.ts +107 -0
  28. package/src/app/api/auth/reset_password/route.ts +107 -0
  29. package/src/app/api/auth/update_user/route.ts +126 -0
  30. package/src/app/api/auth/upload_profile_picture/route.ts +268 -0
  31. package/src/app/api/auth/validate_reset_token/route.ts +80 -0
  32. package/src/app/api/auth/verify_email/route.ts +85 -0
  33. package/src/app/api/migrations/apply/route.ts +91 -0
  34. package/src/app/favicon.ico +0 -0
  35. package/src/app/fonts/GeistMonoVF.woff +0 -0
  36. package/src/app/fonts/GeistVF.woff +0 -0
  37. package/src/app/forgot_password/forgot_password_page_client.tsx +60 -0
  38. package/src/app/forgot_password/page.tsx +24 -0
  39. package/src/app/globals.css +89 -0
  40. package/src/app/hazo_connect/api/sqlite/data/route.ts +197 -0
  41. package/src/app/hazo_connect/api/sqlite/schema/route.ts +35 -0
  42. package/src/app/hazo_connect/api/sqlite/tables/route.ts +26 -0
  43. package/src/app/hazo_connect/sqlite_admin/page.tsx +51 -0
  44. package/src/app/hazo_connect/sqlite_admin/sqlite-admin-client.tsx +947 -0
  45. package/src/app/layout.tsx +43 -0
  46. package/src/app/login/login_page_client.tsx +71 -0
  47. package/src/app/login/page.tsx +26 -0
  48. package/src/app/my_settings/my_settings_page_client.tsx +120 -0
  49. package/src/app/my_settings/page.tsx +40 -0
  50. package/src/app/page.tsx +170 -0
  51. package/src/app/register/page.tsx +26 -0
  52. package/src/app/register/register_page_client.tsx +72 -0
  53. package/src/app/reset_password/page.tsx +29 -0
  54. package/src/app/reset_password/reset_password_page_client.tsx +81 -0
  55. package/src/app/verify_email/page.tsx +24 -0
  56. package/src/app/verify_email/verify_email_page_client.tsx +60 -0
  57. package/src/components/layouts/email_verification/config/email_verification_field_config.ts +86 -0
  58. package/src/components/layouts/email_verification/hooks/use_email_verification.ts +291 -0
  59. package/src/components/layouts/email_verification/index.tsx +297 -0
  60. package/src/components/layouts/forgot_password/config/forgot_password_field_config.ts +58 -0
  61. package/src/components/layouts/forgot_password/hooks/use_forgot_password_form.ts +179 -0
  62. package/src/components/layouts/forgot_password/index.tsx +168 -0
  63. package/src/components/layouts/login/config/login_field_config.ts +67 -0
  64. package/src/components/layouts/login/hooks/use_login_form.ts +281 -0
  65. package/src/components/layouts/login/index.tsx +224 -0
  66. package/src/components/layouts/my_settings/components/editable_field.tsx +177 -0
  67. package/src/components/layouts/my_settings/components/password_change_dialog.tsx +301 -0
  68. package/src/components/layouts/my_settings/components/profile_picture_dialog.tsx +385 -0
  69. package/src/components/layouts/my_settings/components/profile_picture_display.tsx +66 -0
  70. package/src/components/layouts/my_settings/components/profile_picture_gravatar_tab.tsx +143 -0
  71. package/src/components/layouts/my_settings/components/profile_picture_library_tab.tsx +282 -0
  72. package/src/components/layouts/my_settings/components/profile_picture_upload_tab.tsx +341 -0
  73. package/src/components/layouts/my_settings/config/my_settings_field_config.ts +61 -0
  74. package/src/components/layouts/my_settings/hooks/use_my_settings.ts +458 -0
  75. package/src/components/layouts/my_settings/index.tsx +351 -0
  76. package/src/components/layouts/register/config/register_field_config.ts +101 -0
  77. package/src/components/layouts/register/hooks/use_register_form.ts +272 -0
  78. package/src/components/layouts/register/index.tsx +208 -0
  79. package/src/components/layouts/reset_password/config/reset_password_field_config.ts +86 -0
  80. package/src/components/layouts/reset_password/hooks/use_reset_password_form.ts +276 -0
  81. package/src/components/layouts/reset_password/index.tsx +294 -0
  82. package/src/components/layouts/shared/components/already_logged_in_guard.tsx +95 -0
  83. package/src/components/layouts/shared/components/field_error_message.tsx +29 -0
  84. package/src/components/layouts/shared/components/form_action_buttons.tsx +64 -0
  85. package/src/components/layouts/shared/components/form_field_wrapper.tsx +44 -0
  86. package/src/components/layouts/shared/components/form_header.tsx +36 -0
  87. package/src/components/layouts/shared/components/logout_button.tsx +76 -0
  88. package/src/components/layouts/shared/components/password_field.tsx +72 -0
  89. package/src/components/layouts/shared/components/sidebar_layout_wrapper.tsx +264 -0
  90. package/src/components/layouts/shared/components/two_column_auth_layout.tsx +44 -0
  91. package/src/components/layouts/shared/components/unauthorized_guard.tsx +78 -0
  92. package/src/components/layouts/shared/components/visual_panel.tsx +41 -0
  93. package/src/components/layouts/shared/config/layout_customization.ts +95 -0
  94. package/src/components/layouts/shared/data/layout_data_client.ts +19 -0
  95. package/src/components/layouts/shared/hooks/use_auth_status.ts +103 -0
  96. package/src/components/layouts/shared/utils/ip_address.ts +37 -0
  97. package/src/components/layouts/shared/utils/validation.ts +66 -0
  98. package/src/components/ui/avatar.tsx +50 -0
  99. package/src/components/ui/button.tsx +57 -0
  100. package/src/components/ui/dialog.tsx +122 -0
  101. package/src/components/ui/hazo_ui_tooltip.tsx +67 -0
  102. package/src/components/ui/input.tsx +22 -0
  103. package/src/components/ui/label.tsx +26 -0
  104. package/src/components/ui/separator.tsx +31 -0
  105. package/src/components/ui/sheet.tsx +139 -0
  106. package/src/components/ui/sidebar.tsx +773 -0
  107. package/src/components/ui/skeleton.tsx +15 -0
  108. package/src/components/ui/sonner.tsx +31 -0
  109. package/src/components/ui/switch.tsx +29 -0
  110. package/src/components/ui/tabs.tsx +55 -0
  111. package/src/components/ui/tooltip.tsx +32 -0
  112. package/src/components/ui/vertical-tabs.tsx +59 -0
  113. package/src/hooks/use-mobile.tsx +19 -0
  114. package/src/lib/already_logged_in_config.server.ts +46 -0
  115. package/src/lib/app_logger.ts +24 -0
  116. package/src/lib/auth/auth_utils.server.ts +196 -0
  117. package/src/lib/auth/server_auth.ts +88 -0
  118. package/src/lib/config/config_loader.server.ts +149 -0
  119. package/src/lib/email_verification_config.server.ts +32 -0
  120. package/src/lib/file_types_config.server.ts +25 -0
  121. package/src/lib/forgot_password_config.server.ts +32 -0
  122. package/src/lib/hazo_connect_instance.server.ts +77 -0
  123. package/src/lib/hazo_connect_setup.server.ts +181 -0
  124. package/src/lib/hazo_connect_setup.ts +54 -0
  125. package/src/lib/login_config.server.ts +46 -0
  126. package/src/lib/messages_config.server.ts +45 -0
  127. package/src/lib/migrations/apply_migration.ts +105 -0
  128. package/src/lib/my_settings_config.server.ts +135 -0
  129. package/src/lib/password_requirements_config.server.ts +39 -0
  130. package/src/lib/profile_picture_config.server.ts +56 -0
  131. package/src/lib/register_config.server.ts +57 -0
  132. package/src/lib/reset_password_config.server.ts +75 -0
  133. package/src/lib/services/email_service.ts +581 -0
  134. package/src/lib/services/email_verification_service.ts +264 -0
  135. package/src/lib/services/login_service.ts +118 -0
  136. package/src/lib/services/password_change_service.ts +154 -0
  137. package/src/lib/services/password_reset_service.ts +405 -0
  138. package/src/lib/services/profile_picture_remove_service.ts +120 -0
  139. package/src/lib/services/profile_picture_service.ts +215 -0
  140. package/src/lib/services/profile_picture_source_mapper.ts +62 -0
  141. package/src/lib/services/registration_service.ts +163 -0
  142. package/src/lib/services/token_service.ts +240 -0
  143. package/src/lib/services/user_update_service.ts +128 -0
  144. package/src/lib/ui_sizes_config.server.ts +37 -0
  145. package/src/lib/user_fields_config.server.ts +31 -0
  146. package/src/lib/utils/api_route_helpers.ts +60 -0
  147. package/src/lib/utils.ts +11 -0
  148. package/src/middleware.ts +91 -0
  149. package/src/server/config/config_loader.ts +496 -0
  150. package/src/server/index.ts +38 -0
  151. package/src/server/logging/logger_service.ts +56 -0
  152. package/src/server/routes/root_router.ts +16 -0
  153. package/src/server/server.ts +28 -0
  154. package/src/server/types/app_types.ts +74 -0
  155. package/src/server/types/express.d.ts +15 -0
  156. package/src/stories/email_verification_layout.stories.tsx +137 -0
  157. package/src/stories/forgot_password_layout.stories.tsx +85 -0
  158. package/src/stories/login_layout.stories.tsx +85 -0
  159. package/src/stories/project_overview.stories.tsx +33 -0
  160. package/src/stories/register_layout.stories.tsx +107 -0
  161. package/tailwind.config.ts +77 -0
  162. package/tsconfig.json +27 -0
@@ -0,0 +1,64 @@
1
+ // file_description: reusable form action buttons component with submit (positive, left) and cancel (negative, right) buttons
2
+ // section: imports
3
+ import { CircleCheckBig, CircleX } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import type { ButtonPaletteDefaults } from "@/components/layouts/shared/config/layout_customization";
6
+
7
+ // section: types
8
+ type FormActionButtonsProps = {
9
+ submitLabel: string;
10
+ cancelLabel: string;
11
+ buttonPalette: ButtonPaletteDefaults;
12
+ isSubmitDisabled: boolean;
13
+ onCancel: () => void;
14
+ submitAriaLabel?: string;
15
+ cancelAriaLabel?: string;
16
+ className?: string;
17
+ };
18
+
19
+ // section: component
20
+ export function FormActionButtons({
21
+ submitLabel,
22
+ cancelLabel,
23
+ buttonPalette,
24
+ isSubmitDisabled,
25
+ onCancel,
26
+ submitAriaLabel = "Submit form",
27
+ cancelAriaLabel = "Cancel form",
28
+ className,
29
+ }: FormActionButtonsProps) {
30
+ return (
31
+ <div
32
+ className={`cls_form_action_buttons mt-2 flex items-center justify-end gap-4 ${className ?? ""}`}
33
+ >
34
+ <Button
35
+ type="submit"
36
+ disabled={isSubmitDisabled}
37
+ className="cls_form_action_submit_button flex items-center gap-2"
38
+ aria-label={submitAriaLabel}
39
+ style={{
40
+ backgroundColor: buttonPalette.submitBackground,
41
+ color: buttonPalette.submitText,
42
+ }}
43
+ >
44
+ <CircleCheckBig className="h-4 w-4" aria-hidden="true" />
45
+ <span>{submitLabel}</span>
46
+ </Button>
47
+ <Button
48
+ type="button"
49
+ variant="outline"
50
+ onClick={onCancel}
51
+ className="cls_form_action_cancel_button flex items-center gap-2"
52
+ aria-label={cancelAriaLabel}
53
+ style={{
54
+ borderColor: buttonPalette.cancelBorder,
55
+ color: buttonPalette.cancelText,
56
+ }}
57
+ >
58
+ <CircleX className="h-4 w-4" aria-hidden="true" />
59
+ <span>{cancelLabel}</span>
60
+ </Button>
61
+ </div>
62
+ );
63
+ }
64
+
@@ -0,0 +1,44 @@
1
+ // file_description: reusable wrapper component for form fields that standardizes label, input, and error message structure
2
+ // section: imports
3
+ import { Label } from "@/components/ui/label";
4
+ import { FieldErrorMessage } from "@/components/layouts/shared/components/field_error_message";
5
+
6
+ // section: types
7
+ type FormFieldWrapperProps = {
8
+ fieldId: string;
9
+ label: string;
10
+ input: React.ReactNode;
11
+ errorMessage?: string | string[];
12
+ className?: string;
13
+ labelClassName?: string;
14
+ };
15
+
16
+ // section: component
17
+ export function FormFieldWrapper({
18
+ fieldId,
19
+ label,
20
+ input,
21
+ errorMessage,
22
+ className,
23
+ labelClassName,
24
+ }: FormFieldWrapperProps) {
25
+ return (
26
+ <div
27
+ className={`cls_form_field_wrapper flex flex-col gap-2 ${className ?? ""}`}
28
+ >
29
+ <Label
30
+ htmlFor={fieldId}
31
+ className={`cls_form_field_label text-sm font-medium text-slate-800 ${labelClassName ?? ""}`}
32
+ >
33
+ {label}
34
+ </Label>
35
+ {input}
36
+ {errorMessage ? (
37
+ <div className="mt-1 min-h-0">
38
+ <FieldErrorMessage message={errorMessage} />
39
+ </div>
40
+ ) : null}
41
+ </div>
42
+ );
43
+ }
44
+
@@ -0,0 +1,36 @@
1
+ // file_description: reusable form header component for displaying heading and subheading in authentication layouts
2
+ // section: types
3
+ type FormHeaderProps = {
4
+ heading: string;
5
+ subHeading: string;
6
+ className?: string;
7
+ headingClassName?: string;
8
+ subHeadingClassName?: string;
9
+ };
10
+
11
+ // section: component
12
+ export function FormHeader({
13
+ heading,
14
+ subHeading,
15
+ className,
16
+ headingClassName,
17
+ subHeadingClassName,
18
+ }: FormHeaderProps) {
19
+ return (
20
+ <header
21
+ className={`cls_form_header flex flex-col gap-2 text-center md:text-left ${className ?? ""}`}
22
+ >
23
+ <h1
24
+ className={`cls_form_header_title text-2xl font-semibold text-slate-900 ${headingClassName ?? ""}`}
25
+ >
26
+ {heading}
27
+ </h1>
28
+ <p
29
+ className={`cls_form_header_subtitle text-sm text-slate-600 ${subHeadingClassName ?? ""}`}
30
+ >
31
+ {subHeading}
32
+ </p>
33
+ </header>
34
+ );
35
+ }
36
+
@@ -0,0 +1,76 @@
1
+ // file_description: logout button component for user logout functionality
2
+ // section: client_directive
3
+ "use client";
4
+
5
+ // section: imports
6
+ import { useState } from "react";
7
+ import { useRouter } from "next/navigation";
8
+ import { Button } from "@/components/ui/button";
9
+ import { LogOut } from "lucide-react";
10
+ import { toast } from "sonner";
11
+ import { trigger_auth_status_refresh } from "@/components/layouts/shared/hooks/use_auth_status";
12
+
13
+ // section: types
14
+ export type LogoutButtonProps = {
15
+ className?: string;
16
+ variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
17
+ size?: "default" | "sm" | "lg" | "icon";
18
+ };
19
+
20
+ // section: component
21
+ export function LogoutButton({
22
+ className,
23
+ variant = "outline",
24
+ size = "default",
25
+ }: LogoutButtonProps) {
26
+ const router = useRouter();
27
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
28
+
29
+ const handleLogout = async () => {
30
+ setIsLoggingOut(true);
31
+
32
+ try {
33
+ const response = await fetch("/api/auth/logout", {
34
+ method: "POST",
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ },
38
+ });
39
+
40
+ const data = await response.json();
41
+
42
+ if (!response.ok || !data.success) {
43
+ throw new Error(data.error || "Logout failed");
44
+ }
45
+
46
+ toast.success("Logged out successfully");
47
+
48
+ // Trigger auth status refresh in all components (navbar, sidebar, etc.)
49
+ trigger_auth_status_refresh();
50
+
51
+ // Refresh the page to update authentication state (cookies are cleared server-side)
52
+ router.refresh();
53
+ } catch (error) {
54
+ const errorMessage =
55
+ error instanceof Error ? error.message : "Logout failed. Please try again.";
56
+ toast.error(errorMessage);
57
+ } finally {
58
+ setIsLoggingOut(false);
59
+ }
60
+ };
61
+
62
+ return (
63
+ <Button
64
+ onClick={handleLogout}
65
+ disabled={isLoggingOut}
66
+ variant={variant}
67
+ size={size}
68
+ className={className}
69
+ aria-label="Logout"
70
+ >
71
+ <LogOut className="h-4 w-4 mr-2" aria-hidden="true" />
72
+ {isLoggingOut ? "Logging out..." : "Logout"}
73
+ </Button>
74
+ );
75
+ }
76
+
@@ -0,0 +1,72 @@
1
+ // file_description: reusable password input with visibility toggle and error messaging
2
+ // section: client_directive
3
+ "use client";
4
+
5
+ // section: imports
6
+ import { Eye, EyeOff } from "lucide-react";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Input } from "@/components/ui/input";
9
+ import { FieldErrorMessage } from "@/components/layouts/shared/components/field_error_message";
10
+
11
+ // section: types
12
+ export type PasswordFieldProps = {
13
+ inputId: string;
14
+ ariaLabel: string;
15
+ value: string;
16
+ placeholder: string;
17
+ autoComplete?: string;
18
+ isVisible: boolean;
19
+ onChange: (value: string) => void;
20
+ onToggleVisibility: () => void;
21
+ errorMessage?: string | string[];
22
+ };
23
+
24
+ // section: component
25
+ export function PasswordField({
26
+ inputId,
27
+ ariaLabel,
28
+ value,
29
+ placeholder,
30
+ autoComplete,
31
+ isVisible,
32
+ onChange,
33
+ onToggleVisibility,
34
+ errorMessage,
35
+ }: PasswordFieldProps) {
36
+ return (
37
+ <div className="cls_password_field_wrapper">
38
+ <div className="relative">
39
+ <Input
40
+ id={inputId}
41
+ type={isVisible ? "text" : "password"}
42
+ value={value}
43
+ onChange={(event) => onChange(event.target.value)}
44
+ autoComplete={autoComplete}
45
+ placeholder={placeholder}
46
+ aria-label={ariaLabel}
47
+ className="cls_password_field_input pr-11"
48
+ />
49
+ <Button
50
+ type="button"
51
+ variant="ghost"
52
+ size="icon"
53
+ aria-label={`${isVisible ? "Hide" : "Show"} ${ariaLabel.toLowerCase()}`}
54
+ onClick={onToggleVisibility}
55
+ className="cls_password_field_toggle absolute right-1 top-1/2 -translate-y-1/2 text-slate-600 hover:text-slate-900"
56
+ >
57
+ {isVisible ? (
58
+ <EyeOff className="h-4 w-4" aria-hidden="true" />
59
+ ) : (
60
+ <Eye className="h-4 w-4" aria-hidden="true" />
61
+ )}
62
+ </Button>
63
+ </div>
64
+ {errorMessage ? (
65
+ <div className="mt-1 min-h-0">
66
+ <FieldErrorMessage message={errorMessage} />
67
+ </div>
68
+ ) : null}
69
+ </div>
70
+ );
71
+ }
72
+
@@ -0,0 +1,264 @@
1
+ // file_description: shared sidebar layout wrapper for auth pages to ensure consistent navigation
2
+ // section: client_directive
3
+ "use client";
4
+
5
+ // section: imports
6
+ import Link from "next/link";
7
+ import {
8
+ Sidebar,
9
+ SidebarContent,
10
+ SidebarGroup,
11
+ SidebarGroupLabel,
12
+ SidebarHeader,
13
+ SidebarMenu,
14
+ SidebarMenuButton,
15
+ SidebarMenuItem,
16
+ SidebarProvider,
17
+ SidebarTrigger,
18
+ SidebarInset,
19
+ } from "@/components/ui/sidebar";
20
+ import { LogIn, UserPlus, BookOpen, ExternalLink, Database, KeyRound, MailCheck, User, LogOut, Key, Settings } from "lucide-react";
21
+ import { use_auth_status, trigger_auth_status_refresh } from "@/components/layouts/shared/hooks/use_auth_status";
22
+ import { LogoutButton } from "@/components/layouts/shared/components/logout_button";
23
+ import { useRouter } from "next/navigation";
24
+ import { toast } from "sonner";
25
+
26
+ // section: types
27
+ type SidebarLayoutWrapperProps = {
28
+ children: React.ReactNode;
29
+ };
30
+
31
+ // section: component
32
+ export function SidebarLayoutWrapper({ children }: SidebarLayoutWrapperProps) {
33
+ const authStatus = use_auth_status();
34
+ const router = useRouter();
35
+
36
+ const handleLogout = async () => {
37
+ try {
38
+ const response = await fetch("/api/auth/logout", {
39
+ method: "POST",
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ },
43
+ });
44
+
45
+ const data = await response.json();
46
+
47
+ if (!response.ok || !data.success) {
48
+ throw new Error(data.error || "Logout failed");
49
+ }
50
+
51
+ toast.success("Logged out successfully");
52
+
53
+ // Trigger auth status refresh in all components (navbar, sidebar, etc.)
54
+ trigger_auth_status_refresh();
55
+
56
+ // Refresh the page to update authentication state (cookies are cleared server-side)
57
+ router.refresh();
58
+ } catch (error) {
59
+ const errorMessage =
60
+ error instanceof Error ? error.message : "Logout failed. Please try again.";
61
+ toast.error(errorMessage);
62
+ }
63
+ };
64
+
65
+ return (
66
+ <SidebarProvider>
67
+ <div className="cls_sidebar_layout_wrapper flex min-h-screen w-full">
68
+ <Sidebar>
69
+ <SidebarHeader className="cls_sidebar_layout_header">
70
+ <div className="cls_sidebar_layout_title flex items-center gap-2 px-2 py-4">
71
+ <h1 className="cls_sidebar_layout_title_text text-lg font-semibold text-sidebar-foreground">
72
+ hazo auth
73
+ </h1>
74
+ </div>
75
+ </SidebarHeader>
76
+ <SidebarContent className="cls_sidebar_layout_content">
77
+ <SidebarGroup className="cls_sidebar_layout_test_group">
78
+ <SidebarGroupLabel className="cls_sidebar_layout_group_label">
79
+ Test components
80
+ </SidebarGroupLabel>
81
+ <SidebarMenu className="cls_sidebar_layout_test_menu">
82
+ <SidebarMenuItem className="cls_sidebar_layout_test_login_item">
83
+ <SidebarMenuButton asChild>
84
+ <Link
85
+ href="/login"
86
+ className="cls_sidebar_layout_test_login_link flex items-center gap-2"
87
+ aria-label="Test login layout component"
88
+ >
89
+ <LogIn className="h-4 w-4" aria-hidden="true" />
90
+ <span>Test login</span>
91
+ </Link>
92
+ </SidebarMenuButton>
93
+ </SidebarMenuItem>
94
+ <SidebarMenuItem className="cls_sidebar_layout_test_register_item">
95
+ <SidebarMenuButton asChild>
96
+ <Link
97
+ href="/register"
98
+ className="cls_sidebar_layout_test_register_link flex items-center gap-2"
99
+ aria-label="Test register layout component"
100
+ >
101
+ <UserPlus className="h-4 w-4" aria-hidden="true" />
102
+ <span>Test register</span>
103
+ </Link>
104
+ </SidebarMenuButton>
105
+ </SidebarMenuItem>
106
+ <SidebarMenuItem className="cls_sidebar_layout_test_forgot_password_item">
107
+ <SidebarMenuButton asChild>
108
+ <Link
109
+ href="/forgot_password"
110
+ className="cls_sidebar_layout_test_forgot_password_link flex items-center gap-2"
111
+ aria-label="Test forgot password layout component"
112
+ >
113
+ <KeyRound className="h-4 w-4" aria-hidden="true" />
114
+ <span>Test forgot password</span>
115
+ </Link>
116
+ </SidebarMenuButton>
117
+ </SidebarMenuItem>
118
+ <SidebarMenuItem className="cls_sidebar_layout_test_reset_password_item">
119
+ <SidebarMenuButton asChild>
120
+ <Link
121
+ href="/reset_password"
122
+ className="cls_sidebar_layout_test_reset_password_link flex items-center gap-2"
123
+ aria-label="Test reset password layout component"
124
+ >
125
+ <Key className="h-4 w-4" aria-hidden="true" />
126
+ <span>Test reset password</span>
127
+ </Link>
128
+ </SidebarMenuButton>
129
+ </SidebarMenuItem>
130
+ <SidebarMenuItem className="cls_sidebar_layout_test_email_verification_item">
131
+ <SidebarMenuButton asChild>
132
+ <Link
133
+ href="/verify_email"
134
+ className="cls_sidebar_layout_test_email_verification_link flex items-center gap-2"
135
+ aria-label="Test email verification layout component"
136
+ >
137
+ <MailCheck className="h-4 w-4" aria-hidden="true" />
138
+ <span>Test email verification</span>
139
+ </Link>
140
+ </SidebarMenuButton>
141
+ </SidebarMenuItem>
142
+ <SidebarMenuItem className="cls_sidebar_layout_sqlite_admin_item">
143
+ <SidebarMenuButton asChild>
144
+ <Link
145
+ href="/hazo_connect/sqlite_admin"
146
+ className="cls_sidebar_layout_sqlite_admin_link flex items-center gap-2"
147
+ aria-label="Open SQLite admin UI to browse and edit database"
148
+ >
149
+ <Database className="h-4 w-4" aria-hidden="true" />
150
+ <span>SQLite Admin</span>
151
+ </Link>
152
+ </SidebarMenuButton>
153
+ </SidebarMenuItem>
154
+ </SidebarMenu>
155
+ </SidebarGroup>
156
+ {authStatus.authenticated && (
157
+ <SidebarGroup className="cls_sidebar_layout_account_group">
158
+ <SidebarGroupLabel className="cls_sidebar_layout_group_label">
159
+ Account
160
+ </SidebarGroupLabel>
161
+ <SidebarMenu className="cls_sidebar_layout_account_menu">
162
+ <SidebarMenuItem className="cls_sidebar_layout_my_settings_item">
163
+ <SidebarMenuButton asChild>
164
+ <Link
165
+ href="/my_settings"
166
+ className="cls_sidebar_layout_my_settings_link flex items-center gap-2"
167
+ aria-label="Open my settings page"
168
+ >
169
+ <Settings className="h-4 w-4" aria-hidden="true" />
170
+ <span>My Settings</span>
171
+ </Link>
172
+ </SidebarMenuButton>
173
+ </SidebarMenuItem>
174
+ <SidebarMenuItem className="cls_sidebar_layout_logout_item">
175
+ <SidebarMenuButton
176
+ onClick={handleLogout}
177
+ className="cls_sidebar_layout_logout_link flex items-center gap-2"
178
+ aria-label="Logout"
179
+ >
180
+ <LogOut className="h-4 w-4" aria-hidden="true" />
181
+ <span>Logout</span>
182
+ </SidebarMenuButton>
183
+ </SidebarMenuItem>
184
+ </SidebarMenu>
185
+ </SidebarGroup>
186
+ )}
187
+ <SidebarGroup className="cls_sidebar_layout_resources_group">
188
+ <SidebarGroupLabel className="cls_sidebar_layout_group_label">
189
+ Resources
190
+ </SidebarGroupLabel>
191
+ <SidebarMenu className="cls_sidebar_layout_resources_menu">
192
+ <SidebarMenuItem className="cls_sidebar_layout_storybook_item">
193
+ <SidebarMenuButton asChild>
194
+ <a
195
+ href="http://localhost:6006"
196
+ target="_blank"
197
+ rel="noopener noreferrer"
198
+ className="cls_sidebar_layout_storybook_link flex items-center gap-2"
199
+ aria-label="Open Storybook preview for reusable components"
200
+ >
201
+ <BookOpen className="h-4 w-4" aria-hidden="true" />
202
+ <span>Storybook</span>
203
+ <ExternalLink className="ml-auto h-3 w-3" aria-hidden="true" />
204
+ </a>
205
+ </SidebarMenuButton>
206
+ </SidebarMenuItem>
207
+ <SidebarMenuItem className="cls_sidebar_layout_docs_item">
208
+ <SidebarMenuButton asChild>
209
+ <a
210
+ href="https://ui.shadcn.com/docs"
211
+ target="_blank"
212
+ rel="noopener noreferrer"
213
+ className="cls_sidebar_layout_docs_link flex items-center gap-2"
214
+ aria-label="Review shadcn documentation for styling guidance"
215
+ >
216
+ <BookOpen className="h-4 w-4" aria-hidden="true" />
217
+ <span>Shadcn docs</span>
218
+ <ExternalLink className="ml-auto h-3 w-3" aria-hidden="true" />
219
+ </a>
220
+ </SidebarMenuButton>
221
+ </SidebarMenuItem>
222
+ </SidebarMenu>
223
+ </SidebarGroup>
224
+ </SidebarContent>
225
+ </Sidebar>
226
+ <SidebarInset className="cls_sidebar_layout_inset">
227
+ <header className="cls_sidebar_layout_main_header flex h-16 shrink-0 items-center gap-2 border-b px-4">
228
+ <SidebarTrigger className="cls_sidebar_layout_trigger" />
229
+ <div className="cls_sidebar_layout_main_header_content flex flex-1 items-center gap-2">
230
+ <h2 className="cls_sidebar_layout_main_title text-lg font-semibold text-foreground">
231
+ hazo reusable ui library workspace
232
+ </h2>
233
+ </div>
234
+ <div className="cls_sidebar_layout_auth_status flex items-center gap-3">
235
+ {authStatus.loading ? (
236
+ <span className="cls_sidebar_layout_auth_loading text-sm text-muted-foreground">
237
+ Loading...
238
+ </span>
239
+ ) : authStatus.authenticated ? (
240
+ <>
241
+ <div className="cls_sidebar_layout_user_info flex items-center gap-2">
242
+ <User className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
243
+ <span className="cls_sidebar_layout_user_name text-sm font-medium text-foreground">
244
+ {authStatus.name || authStatus.email || "Logged in"}
245
+ </span>
246
+ </div>
247
+ <LogoutButton size="sm" />
248
+ </>
249
+ ) : (
250
+ <span className="cls_sidebar_layout_not_logged_in text-sm text-muted-foreground">
251
+ Not logged in
252
+ </span>
253
+ )}
254
+ </div>
255
+ </header>
256
+ <main className="cls_sidebar_layout_main_content flex flex-1 items-center justify-center p-6">
257
+ {children}
258
+ </main>
259
+ </SidebarInset>
260
+ </div>
261
+ </SidebarProvider>
262
+ );
263
+ }
264
+
@@ -0,0 +1,44 @@
1
+ // file_description: reusable two-column authentication layout shell that combines visual panel and form content
2
+ // section: imports
3
+ import { VisualPanel } from "@/components/layouts/shared/components/visual_panel";
4
+
5
+ // section: types
6
+ type TwoColumnAuthLayoutProps = {
7
+ imageSrc: string;
8
+ imageAlt: string;
9
+ imageBackgroundColor?: string;
10
+ formContent: React.ReactNode;
11
+ className?: string;
12
+ visualPanelClassName?: string;
13
+ formContainerClassName?: string;
14
+ };
15
+
16
+ // section: component
17
+ export function TwoColumnAuthLayout({
18
+ imageSrc,
19
+ imageAlt,
20
+ imageBackgroundColor,
21
+ formContent,
22
+ className,
23
+ visualPanelClassName,
24
+ formContainerClassName,
25
+ }: TwoColumnAuthLayoutProps) {
26
+ return (
27
+ <div
28
+ className={`cls_two_column_auth_layout mx-auto grid w-full max-w-5xl grid-cols-1 overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm md:grid-cols-2 md:min-h-[520px] ${className ?? ""}`}
29
+ >
30
+ <VisualPanel
31
+ imageSrc={imageSrc}
32
+ imageAlt={imageAlt}
33
+ backgroundColor={imageBackgroundColor}
34
+ className={visualPanelClassName}
35
+ />
36
+ <div
37
+ className={`cls_two_column_auth_layout_form_container flex flex-col gap-6 p-8 ${formContainerClassName ?? ""}`}
38
+ >
39
+ {formContent}
40
+ </div>
41
+ </div>
42
+ );
43
+ }
44
+
@@ -0,0 +1,78 @@
1
+ // file_description: reusable component to show unauthorized message when user is not authenticated
2
+ // section: client_directive
3
+ "use client";
4
+
5
+ // section: imports
6
+ import { use_auth_status } from "@/components/layouts/shared/hooks/use_auth_status";
7
+ import { Button } from "@/components/ui/button";
8
+ import { useRouter } from "next/navigation";
9
+ import { LogIn } from "lucide-react";
10
+
11
+ // section: types
12
+ export type UnauthorizedGuardProps = {
13
+ message?: string;
14
+ loginButtonLabel?: string;
15
+ loginPath?: string;
16
+ children: React.ReactNode;
17
+ };
18
+
19
+ // section: component
20
+ /**
21
+ * Guard component that shows unauthorized message if user is not authenticated
22
+ * Otherwise renders children
23
+ * @param props - Component props including message and login button customization
24
+ * @returns Either the unauthorized UI or the children
25
+ */
26
+ export function UnauthorizedGuard({
27
+ message = "You must be logged in to access this page.",
28
+ loginButtonLabel = "Go to login",
29
+ loginPath = "/login",
30
+ children,
31
+ }: UnauthorizedGuardProps) {
32
+ const router = useRouter();
33
+ const authStatus = use_auth_status();
34
+
35
+ // Check if user should see unauthorized message
36
+ const shouldShowUnauthorized = !authStatus.authenticated && !authStatus.loading;
37
+
38
+ if (shouldShowUnauthorized) {
39
+ return (
40
+ <div className="cls_unauthorized_guard flex flex-col items-center justify-center min-h-screen p-8">
41
+ <div className="cls_unauthorized_guard_content flex flex-col items-center gap-4 text-center max-w-md">
42
+ <div className="cls_unauthorized_guard_icon flex items-center justify-center w-16 h-16 rounded-full bg-red-100">
43
+ <LogIn className="h-8 w-8 text-red-600" aria-hidden="true" />
44
+ </div>
45
+ <div className="cls_unauthorized_guard_text">
46
+ <h1 className="cls_unauthorized_guard_heading text-2xl font-semibold text-slate-900 mb-2">
47
+ Access Denied
48
+ </h1>
49
+ <p className="cls_unauthorized_guard_message text-slate-600">
50
+ {message}
51
+ </p>
52
+ </div>
53
+ <Button
54
+ onClick={() => router.push(loginPath)}
55
+ variant="default"
56
+ className="cls_unauthorized_guard_login_button mt-4"
57
+ aria-label={loginButtonLabel}
58
+ >
59
+ <LogIn className="h-4 w-4 mr-2" aria-hidden="true" />
60
+ {loginButtonLabel}
61
+ </Button>
62
+ </div>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ // Show loading state while checking authentication
68
+ if (authStatus.loading) {
69
+ return (
70
+ <div className="cls_unauthorized_guard_loading flex items-center justify-center min-h-screen">
71
+ <div className="cls_unauthorized_guard_loading_text text-slate-600">Loading...</div>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ return <>{children}</>;
77
+ }
78
+