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,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
|
+
|