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,15 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({
4
+ className,
5
+ ...props
6
+ }: React.HTMLAttributes<HTMLDivElement>) {
7
+ return (
8
+ <div
9
+ className={cn("animate-pulse rounded-md bg-primary/10", className)}
10
+ {...props}
11
+ />
12
+ )
13
+ }
14
+
15
+ export { Skeleton }
@@ -0,0 +1,31 @@
1
+ "use client"
2
+
3
+ import { useTheme } from "next-themes"
4
+ import { Toaster as Sonner } from "sonner"
5
+
6
+ type ToasterProps = React.ComponentProps<typeof Sonner>
7
+
8
+ const Toaster = ({ ...props }: ToasterProps) => {
9
+ const { theme = "system" } = useTheme()
10
+
11
+ return (
12
+ <Sonner
13
+ theme={theme as ToasterProps["theme"]}
14
+ className="toaster group"
15
+ toastOptions={{
16
+ classNames: {
17
+ toast:
18
+ "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
19
+ description: "group-[.toast]:text-muted-foreground",
20
+ actionButton:
21
+ "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
22
+ cancelButton:
23
+ "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
24
+ },
25
+ }}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ export { Toaster }
@@ -0,0 +1,29 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SwitchPrimitives from "@radix-ui/react-switch"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Switch = React.forwardRef<
9
+ React.ElementRef<typeof SwitchPrimitives.Root>,
10
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <SwitchPrimitives.Root
13
+ className={cn(
14
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
15
+ className
16
+ )}
17
+ {...props}
18
+ ref={ref}
19
+ >
20
+ <SwitchPrimitives.Thumb
21
+ className={cn(
22
+ "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
23
+ )}
24
+ />
25
+ </SwitchPrimitives.Root>
26
+ ))
27
+ Switch.displayName = SwitchPrimitives.Root.displayName
28
+
29
+ export { Switch }
@@ -0,0 +1,55 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Tabs = TabsPrimitive.Root
9
+
10
+ const TabsList = React.forwardRef<
11
+ React.ElementRef<typeof TabsPrimitive.List>,
12
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
13
+ >(({ className, ...props }, ref) => (
14
+ <TabsPrimitive.List
15
+ ref={ref}
16
+ className={cn(
17
+ "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ ))
23
+ TabsList.displayName = TabsPrimitive.List.displayName
24
+
25
+ const TabsTrigger = React.forwardRef<
26
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
27
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
28
+ >(({ className, ...props }, ref) => (
29
+ <TabsPrimitive.Trigger
30
+ ref={ref}
31
+ className={cn(
32
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
33
+ className
34
+ )}
35
+ {...props}
36
+ />
37
+ ))
38
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39
+
40
+ const TabsContent = React.forwardRef<
41
+ React.ElementRef<typeof TabsPrimitive.Content>,
42
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
43
+ >(({ className, ...props }, ref) => (
44
+ <TabsPrimitive.Content
45
+ ref={ref}
46
+ className={cn(
47
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ ))
53
+ TabsContent.displayName = TabsPrimitive.Content.displayName
54
+
55
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -0,0 +1,32 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const TooltipProvider = TooltipPrimitive.Provider
9
+
10
+ const Tooltip = TooltipPrimitive.Root
11
+
12
+ const TooltipTrigger = TooltipPrimitive.Trigger
13
+
14
+ const TooltipContent = React.forwardRef<
15
+ React.ElementRef<typeof TooltipPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
+ >(({ className, sideOffset = 4, ...props }, ref) => (
18
+ <TooltipPrimitive.Portal>
19
+ <TooltipPrimitive.Content
20
+ ref={ref}
21
+ sideOffset={sideOffset}
22
+ className={cn(
23
+ "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ </TooltipPrimitive.Portal>
29
+ ))
30
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
31
+
32
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,59 @@
1
+ // file_description: vertical tabs component for sidebar-style navigation
2
+ // section: client_directive
3
+ "use client";
4
+
5
+ // section: imports
6
+ import * as React from "react";
7
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
8
+ import { cn } from "@/lib/utils";
9
+
10
+ // section: components
11
+ const VerticalTabs = TabsPrimitive.Root;
12
+
13
+ const VerticalTabsList = React.forwardRef<
14
+ React.ElementRef<typeof TabsPrimitive.List>,
15
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
16
+ >(({ className, ...props }, ref) => (
17
+ <TabsPrimitive.List
18
+ ref={ref}
19
+ className={cn(
20
+ "inline-flex flex-col items-start justify-start rounded-md bg-muted p-1 text-muted-foreground",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ ));
26
+ VerticalTabsList.displayName = "VerticalTabsList";
27
+
28
+ const VerticalTabsTrigger = React.forwardRef<
29
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
30
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
31
+ >(({ className, ...props }, ref) => (
32
+ <TabsPrimitive.Trigger
33
+ ref={ref}
34
+ className={cn(
35
+ "inline-flex items-center justify-start whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
36
+ className
37
+ )}
38
+ {...props}
39
+ />
40
+ ));
41
+ VerticalTabsTrigger.displayName = "VerticalTabsTrigger";
42
+
43
+ const VerticalTabsContent = React.forwardRef<
44
+ React.ElementRef<typeof TabsPrimitive.Content>,
45
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
46
+ >(({ className, ...props }, ref) => (
47
+ <TabsPrimitive.Content
48
+ ref={ref}
49
+ className={cn(
50
+ "mt-0 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
51
+ className
52
+ )}
53
+ {...props}
54
+ />
55
+ ));
56
+ VerticalTabsContent.displayName = "VerticalTabsContent";
57
+
58
+ export { VerticalTabs, VerticalTabsList, VerticalTabsTrigger, VerticalTabsContent };
59
+
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
@@ -0,0 +1,46 @@
1
+ // file_description: server-only helper to read already logged in configuration from hazo_auth_config.ini
2
+ // section: imports
3
+ import { get_config_value, get_config_boolean } from "./config/config_loader.server";
4
+
5
+ // section: types
6
+ export type AlreadyLoggedInConfig = {
7
+ message: string;
8
+ showLogoutButton: boolean;
9
+ showReturnHomeButton: boolean;
10
+ returnHomeButtonLabel: string;
11
+ returnHomePath: string;
12
+ };
13
+
14
+ // section: helpers
15
+ /**
16
+ * Reads already logged in configuration from hazo_auth_config.ini file
17
+ * Falls back to defaults if hazo_auth_config.ini is not found or section is missing
18
+ * @returns Already logged in configuration options
19
+ */
20
+ export function get_already_logged_in_config(): AlreadyLoggedInConfig {
21
+ const section = "hazo_auth__already_logged_in";
22
+
23
+ // Read message (defaults to "You're already logged in.")
24
+ const message = get_config_value(section, "message", "You're already logged in.");
25
+
26
+ // Read show logout button (defaults to true)
27
+ const showLogoutButton = get_config_boolean(section, "show_logout_button", true);
28
+
29
+ // Read show return home button (defaults to false)
30
+ const showReturnHomeButton = get_config_boolean(section, "show_return_home_button", false);
31
+
32
+ // Read return home button label (defaults to "Return home")
33
+ const returnHomeButtonLabel = get_config_value(section, "return_home_button_label", "Return home");
34
+
35
+ // Read return home path (defaults to "/")
36
+ const returnHomePath = get_config_value(section, "return_home_path", "/");
37
+
38
+ return {
39
+ message,
40
+ showLogoutButton,
41
+ showReturnHomeButton,
42
+ returnHomeButtonLabel,
43
+ returnHomePath,
44
+ };
45
+ }
46
+
@@ -0,0 +1,24 @@
1
+ // file_description: client-accessible wrapper for the main app logging service
2
+ // section: imports
3
+ import { create_logger_service } from "@/server/logging/logger_service";
4
+
5
+ // section: constants
6
+ const APP_NAMESPACE = "hazo_auth_ui";
7
+
8
+ // section: logger_instance
9
+ /**
10
+ * Creates a logger service instance for use in UI components
11
+ * This uses the main app logging service and can be extended with an external logger
12
+ * when provided as part of component setup
13
+ */
14
+ export const create_app_logger = (
15
+ external_logger?: {
16
+ info?: (message: string, data?: Record<string, unknown>) => void;
17
+ error?: (message: string, data?: Record<string, unknown>) => void;
18
+ warn?: (message: string, data?: Record<string, unknown>) => void;
19
+ debug?: (message: string, data?: Record<string, unknown>) => void;
20
+ }
21
+ ) => {
22
+ return create_logger_service(APP_NAMESPACE, external_logger);
23
+ };
24
+
@@ -0,0 +1,196 @@
1
+ // file_description: server-side authentication utilities for checking login status in API routes
2
+ // section: imports
3
+ import { NextRequest, NextResponse } from "next/server";
4
+ import { get_hazo_connect_instance } from "../hazo_connect_instance.server";
5
+ import { createCrudService } from "hazo_connect/server";
6
+ import { map_db_source_to_ui } from "../services/profile_picture_source_mapper";
7
+
8
+ // section: types
9
+ export type AuthUser = {
10
+ authenticated: true;
11
+ user_id: string;
12
+ email: string;
13
+ name?: string;
14
+ email_verified: boolean;
15
+ is_active: boolean;
16
+ last_logon?: string;
17
+ profile_picture_url?: string;
18
+ profile_source?: "upload" | "library" | "gravatar" | "custom";
19
+ };
20
+
21
+ export type AuthResult =
22
+ | AuthUser
23
+ | { authenticated: false };
24
+
25
+ // section: helpers
26
+ /**
27
+ * Clears authentication cookies from response
28
+ * @param response - NextResponse object to clear cookies from
29
+ * @returns The response with cleared cookies
30
+ */
31
+ function clear_auth_cookies(response: NextResponse): NextResponse {
32
+ response.cookies.set("hazo_auth_user_email", "", {
33
+ expires: new Date(0),
34
+ path: "/",
35
+ });
36
+ response.cookies.set("hazo_auth_user_id", "", {
37
+ expires: new Date(0),
38
+ path: "/",
39
+ });
40
+ return response;
41
+ }
42
+
43
+ // section: functions
44
+ /**
45
+ * Checks if a user is authenticated from request cookies
46
+ * Validates user exists, is active, and cookies match
47
+ * @param request - NextRequest object
48
+ * @returns AuthResult with user info or authenticated: false
49
+ */
50
+ export async function get_authenticated_user(request: NextRequest): Promise<AuthResult> {
51
+ const user_id = request.cookies.get("hazo_auth_user_id")?.value;
52
+ const user_email = request.cookies.get("hazo_auth_user_email")?.value;
53
+
54
+ if (!user_id || !user_email) {
55
+ return { authenticated: false };
56
+ }
57
+
58
+ try {
59
+ const hazoConnect = get_hazo_connect_instance();
60
+ const users_service = createCrudService(hazoConnect, "hazo_users");
61
+
62
+ const users = await users_service.findBy({
63
+ id: user_id,
64
+ email_address: user_email,
65
+ });
66
+
67
+ if (!Array.isArray(users) || users.length === 0) {
68
+ return { authenticated: false };
69
+ }
70
+
71
+ const user = users[0];
72
+
73
+ // Check if user is active
74
+ if (user.is_active === false) {
75
+ return { authenticated: false };
76
+ }
77
+
78
+ // Map database profile_source to UI representation
79
+ const profile_source_db = user.profile_source as string | null | undefined;
80
+ const profile_source_ui = profile_source_db ? map_db_source_to_ui(profile_source_db) : undefined;
81
+
82
+ return {
83
+ authenticated: true,
84
+ user_id: user.id as string,
85
+ email: user.email_address as string,
86
+ name: (user.name as string | null | undefined) || undefined,
87
+ email_verified: user.email_verified === true,
88
+ is_active: user.is_active === true,
89
+ last_logon: (user.last_logon as string | null | undefined) || undefined,
90
+ profile_picture_url: (user.profile_picture_url as string | null | undefined) || undefined,
91
+ profile_source: profile_source_ui,
92
+ };
93
+ } catch (error) {
94
+ return { authenticated: false };
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Checks if user is authenticated (simple boolean check)
100
+ * @param request - NextRequest object
101
+ * @returns true if authenticated, false otherwise
102
+ */
103
+ export async function is_authenticated(request: NextRequest): Promise<boolean> {
104
+ const result = await get_authenticated_user(request);
105
+ return result.authenticated;
106
+ }
107
+
108
+ /**
109
+ * Requires authentication - throws error if not authenticated
110
+ * Use in API routes that require authentication
111
+ * @param request - NextRequest object
112
+ * @returns AuthUser (never returns authenticated: false, throws instead)
113
+ * @throws Error if not authenticated
114
+ */
115
+ export async function require_auth(request: NextRequest): Promise<AuthUser> {
116
+ const result = await get_authenticated_user(request);
117
+
118
+ if (!result.authenticated) {
119
+ throw new Error("Authentication required");
120
+ }
121
+
122
+ return result;
123
+ }
124
+
125
+ /**
126
+ * Gets authenticated user and returns response with cleared cookies if invalid
127
+ * Useful for /api/auth/me endpoint that needs to clear cookies on invalid auth
128
+ * @param request - NextRequest object
129
+ * @returns Object with auth_result and response (with cleared cookies if invalid)
130
+ */
131
+ export async function get_authenticated_user_with_response(request: NextRequest): Promise<{
132
+ auth_result: AuthResult;
133
+ response?: NextResponse;
134
+ }> {
135
+ const user_id = request.cookies.get("hazo_auth_user_id")?.value;
136
+ const user_email = request.cookies.get("hazo_auth_user_email")?.value;
137
+
138
+ if (!user_id || !user_email) {
139
+ return { auth_result: { authenticated: false } };
140
+ }
141
+
142
+ try {
143
+ const hazoConnect = get_hazo_connect_instance();
144
+ const users_service = createCrudService(hazoConnect, "hazo_users");
145
+
146
+ const users = await users_service.findBy({
147
+ id: user_id,
148
+ email_address: user_email,
149
+ });
150
+
151
+ if (!Array.isArray(users) || users.length === 0) {
152
+ // User not found - clear cookies
153
+ const response = NextResponse.json(
154
+ { authenticated: false },
155
+ { status: 200 }
156
+ );
157
+ clear_auth_cookies(response);
158
+ return { auth_result: { authenticated: false }, response };
159
+ }
160
+
161
+ const user = users[0];
162
+
163
+ // Check if user is still active
164
+ if (user.is_active === false) {
165
+ // User is inactive - clear cookies
166
+ const response = NextResponse.json(
167
+ { authenticated: false },
168
+ { status: 200 }
169
+ );
170
+ clear_auth_cookies(response);
171
+ return { auth_result: { authenticated: false }, response };
172
+ }
173
+
174
+ // Map database profile_source to UI representation
175
+ const profile_source_db = user.profile_source as string | null | undefined;
176
+ const profile_source_ui = profile_source_db ? map_db_source_to_ui(profile_source_db) : undefined;
177
+
178
+ return {
179
+ auth_result: {
180
+ authenticated: true,
181
+ user_id: user.id as string,
182
+ email: user.email_address as string,
183
+ name: (user.name as string | null | undefined) || undefined,
184
+ email_verified: user.email_verified === true,
185
+ is_active: user.is_active === true,
186
+ last_logon: (user.last_logon as string | null | undefined) || undefined,
187
+ profile_picture_url: (user.profile_picture_url as string | null | undefined) || undefined,
188
+ profile_source: profile_source_ui,
189
+ },
190
+ };
191
+ } catch (error) {
192
+ // On error, assume not authenticated
193
+ return { auth_result: { authenticated: false } };
194
+ }
195
+ }
196
+
@@ -0,0 +1,88 @@
1
+ // file_description: server-side auth utilities for server components and pages
2
+ // section: imports
3
+ import { cookies } from "next/headers";
4
+ import { get_hazo_connect_instance } from "../hazo_connect_instance.server";
5
+ import { createCrudService } from "hazo_connect/server";
6
+ import { map_db_source_to_ui } from "../services/profile_picture_source_mapper";
7
+
8
+ // section: types
9
+ export type ServerAuthUser = {
10
+ authenticated: true;
11
+ user_id: string;
12
+ email: string;
13
+ name?: string;
14
+ email_verified: boolean;
15
+ is_active: boolean;
16
+ last_logon?: string;
17
+ profile_picture_url?: string;
18
+ profile_source?: "upload" | "library" | "gravatar" | "custom";
19
+ };
20
+
21
+ export type ServerAuthResult =
22
+ | ServerAuthUser
23
+ | { authenticated: false };
24
+
25
+ // section: functions
26
+ /**
27
+ * Gets authenticated user in server components/pages
28
+ * Uses Next.js cookies() function to read authentication cookies
29
+ * @returns ServerAuthResult with user info or authenticated: false
30
+ */
31
+ export async function get_server_auth_user(): Promise<ServerAuthResult> {
32
+ const cookie_store = await cookies();
33
+ const user_id = cookie_store.get("hazo_auth_user_id")?.value;
34
+ const user_email = cookie_store.get("hazo_auth_user_email")?.value;
35
+
36
+ if (!user_id || !user_email) {
37
+ return { authenticated: false };
38
+ }
39
+
40
+ try {
41
+ const hazoConnect = get_hazo_connect_instance();
42
+ const users_service = createCrudService(hazoConnect, "hazo_users");
43
+
44
+ const users = await users_service.findBy({
45
+ id: user_id,
46
+ email_address: user_email,
47
+ });
48
+
49
+ if (!Array.isArray(users) || users.length === 0) {
50
+ return { authenticated: false };
51
+ }
52
+
53
+ const user = users[0];
54
+
55
+ // Check if user is active
56
+ if (user.is_active === false) {
57
+ return { authenticated: false };
58
+ }
59
+
60
+ // Map database profile_source to UI representation
61
+ const profile_source_db = user.profile_source as string | null | undefined;
62
+ const profile_source_ui = profile_source_db ? map_db_source_to_ui(profile_source_db) : undefined;
63
+
64
+ return {
65
+ authenticated: true,
66
+ user_id: user.id as string,
67
+ email: user.email_address as string,
68
+ name: (user.name as string | null | undefined) || undefined,
69
+ email_verified: user.email_verified === true,
70
+ is_active: user.is_active === true,
71
+ last_logon: (user.last_logon as string | null | undefined) || undefined,
72
+ profile_picture_url: (user.profile_picture_url as string | null | undefined) || undefined,
73
+ profile_source: profile_source_ui,
74
+ };
75
+ } catch (error) {
76
+ return { authenticated: false };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Checks if user is authenticated in server components/pages (simple boolean check)
82
+ * @returns true if authenticated, false otherwise
83
+ */
84
+ export async function is_server_authenticated(): Promise<boolean> {
85
+ const result = await get_server_auth_user();
86
+ return result.authenticated;
87
+ }
88
+