auth flow draft

This commit is contained in:
wass08
2026-02-10 15:58:34 +09:00
parent e8b2b5b2d1
commit c7e5aa48c6
13 changed files with 1070 additions and 1 deletions
+65
View File
@@ -0,0 +1,65 @@
/**
* Auth client for the editor using better-auth
* Connects to the Pascal monorepo backend
*/
import { createAuthClient } from 'better-auth/react'
import {
customSessionClient,
magicLinkClient,
organizationClient,
} from 'better-auth/client/plugins'
/**
* Get the backend API URL
* Default: http://localhost:3000 (monorepo backend)
*/
function getBackendURL(): string {
// Check if we have an env variable for the backend URL
if (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_API_URL) {
return process.env.NEXT_PUBLIC_API_URL
}
// In browser, try to use the current origin if it's the same host
if (typeof window !== 'undefined') {
// For local development, always use localhost:3000 (monorepo backend)
if (window.location.hostname === 'localhost') {
return 'http://localhost:3000'
}
// For production, use the same origin
return window.location.origin
}
// SSR fallback
return 'http://localhost:3000'
}
/**
* Auth client instance with better-auth
* Configured to work with the Pascal monorepo backend
*/
export const authClient = createAuthClient({
baseURL: getBackendURL(),
plugins: [
magicLinkClient(),
organizationClient(),
customSessionClient<{
session: {
activePropertyId: string | null
activeOrganizationId: string | null
}
}>(),
],
})
/**
* Export types for use in components
*/
export type AuthState = {
user: (typeof authClient)['$Infer']['Session']['user'] | null
session: (typeof authClient)['$Infer']['Session']['session'] | null
isLoading: boolean
}
export type User = NonNullable<AuthState['user']>
export type Session = NonNullable<AuthState['session']>
+20
View File
@@ -0,0 +1,20 @@
'use client'
import { authClient } from './auth-client'
/**
* Hook to access authentication state using better-auth
* @returns Current auth state including user, session, and loading status
*/
export function useAuth() {
const session = authClient.useSession()
return {
user: session.data?.user ?? null,
session: session.data?.session ?? null,
isAuthenticated: !!session.data?.user && !!session.data?.session,
isLoading: session.isPending,
signOut: () => authClient.signOut(),
signIn: authClient.signIn,
}
}
+216
View File
@@ -0,0 +1,216 @@
/**
* Property actions - API client for property management
* Makes HTTP requests to the Pascal monorepo backend
*/
import type { CreatePropertyParams, Property } from './types'
export type ActionResult<T = unknown> = {
success: boolean
data?: T
error?: string
message?: string
}
/**
* Get the backend API URL
*/
function getBackendURL(): string {
if (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_API_URL) {
return process.env.NEXT_PUBLIC_API_URL
}
if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
return 'http://localhost:3000'
}
return typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'
}
/**
* Fetch all properties for the current user
*/
export async function getUserProperties(): Promise<ActionResult<Property[]>> {
try {
const response = await fetch(`${getBackendURL()}/api/properties`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const error = await response.text()
return {
success: false,
error: error || 'Failed to fetch properties',
data: [],
}
}
const result = await response.json()
return {
success: true,
data: result.data || [],
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch properties',
data: [],
}
}
}
/**
* Get the active property for the current session
*/
export async function getActiveProperty(): Promise<ActionResult<Property | null>> {
try {
const response = await fetch(`${getBackendURL()}/api/properties/active`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const error = await response.text()
return {
success: false,
error: error || 'Failed to fetch active property',
data: null,
}
}
const result = await response.json()
return {
success: true,
data: result.data || null,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch active property',
data: null,
}
}
}
/**
* Set the active property for the current session
*/
export async function setActiveProperty(propertyId: string | null): Promise<ActionResult> {
try {
const response = await fetch(`${getBackendURL()}/api/properties/active`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ propertyId }),
})
if (!response.ok) {
const error = await response.text()
return {
success: false,
error: error || 'Failed to set active property',
}
}
const result = await response.json()
return {
success: true,
message: result.message || (propertyId ? 'Active property updated' : 'Active property cleared'),
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to set active property',
}
}
}
/**
* Check if a property with the given address already exists
*/
export async function checkPropertyDuplicate(params: {
streetNumber?: string
route?: string
city?: string
state?: string
postalCode?: string
}): Promise<
ActionResult<{
isDuplicate: boolean
isUserProperty?: boolean
existingProperty?: Property
}>
> {
try {
const response = await fetch(`${getBackendURL()}/api/properties/check-duplicate`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
})
if (!response.ok) {
const error = await response.text()
return {
success: false,
error: error || 'Failed to check for duplicates',
}
}
const result = await response.json()
return {
success: true,
data: result.data || { isDuplicate: false },
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to check for duplicates',
}
}
}
/**
* Create a new property
*/
export async function createProperty(params: CreatePropertyParams): Promise<ActionResult<Property>> {
try {
const response = await fetch(`${getBackendURL()}/api/properties`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
})
if (!response.ok) {
const error = await response.text()
return {
success: false,
error: error || 'Failed to create property',
}
}
const result = await response.json()
return {
success: true,
data: result.data,
message: result.message || 'Property created successfully',
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create property',
}
}
}
+43
View File
@@ -0,0 +1,43 @@
/**
* Property-related type definitions
* Isolated from monorepo database schema
*/
export type Property = {
id: string
name: string
ownerId: string
organizationId: string | null
addressId: string
createdAt: Date
updatedAt: Date
address: {
id: string
streetNumber?: string
route?: string
city?: string
state?: string
postalCode?: string
country?: string
latitude?: string
longitude?: string
}
}
export type CreatePropertyParams = {
name: string
center: [number, number]
streetNumber?: string
route?: string
routeShort?: string
neighborhood?: string
city?: string
county?: string
state?: string
stateLong?: string
postalCode?: string
postalCodeSuffix?: string
country?: string
countryLong?: string
rawJson?: Record<string, unknown>
}
@@ -0,0 +1,198 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import {
getActiveProperty,
getUserProperties,
setActiveProperty as setActivePropertyAction,
} from './actions'
import type { Property } from './types'
interface UsePropertiesReturn {
properties: Property[]
isLoading: boolean
error: string | null
refetch: () => Promise<void>
}
/**
* Hook to fetch and manage user properties
*/
export function useProperties(): UsePropertiesReturn {
const [properties, setProperties] = useState<Property[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchProperties = useCallback(async () => {
try {
setIsLoading(true)
setError(null)
const result = await getUserProperties()
if (result.success) {
setProperties(result.data || [])
} else {
setError(result.error || 'Failed to fetch properties')
setProperties([])
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An unexpected error occurred')
setProperties([])
} finally {
setIsLoading(false)
}
}, [])
useEffect(() => {
fetchProperties()
}, [fetchProperties])
return {
properties,
isLoading,
error,
refetch: fetchProperties,
}
}
/**
* Hook to fetch a single property by ID
*/
export function useProperty(propertyId: string | undefined) {
const [property, setProperty] = useState<Property | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!propertyId) {
setProperty(null)
setIsLoading(false)
return
}
const fetchProperty = async () => {
try {
setIsLoading(true)
setError(null)
const result = await getUserProperties()
if (result.success) {
const found = result.data?.find((p) => p.id === propertyId)
if (found) {
setProperty(found)
} else {
setError('Property not found')
setProperty(null)
}
} else {
setError(result.error || 'Failed to fetch property')
setProperty(null)
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An unexpected error occurred')
setProperty(null)
} finally {
setIsLoading(false)
}
}
fetchProperty()
}, [propertyId])
return {
property,
isLoading,
error,
}
}
/**
* Hook to manage the active property for the current session
*/
export function useActiveProperty() {
const [activeProperty, setActivePropertyState] = useState<Property | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isPending, setIsPending] = useState(false)
const fetchActiveProperty = useCallback(async () => {
try {
setIsLoading(true)
setError(null)
const result = await getActiveProperty()
if (result.success) {
setActivePropertyState(result.data || null)
// If no active property is set, automatically set the first property as active
if (!result.data) {
const propertiesResult = await getUserProperties()
if (
propertiesResult.success &&
propertiesResult.data &&
propertiesResult.data.length > 0
) {
const firstProperty = propertiesResult.data[0]
if (firstProperty) {
const setActiveResult = await setActivePropertyAction(firstProperty.id)
if (setActiveResult.success) {
setActivePropertyState(firstProperty)
}
}
}
}
} else {
setError(result.error || 'Failed to fetch active property')
setActivePropertyState(null)
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An unexpected error occurred')
setActivePropertyState(null)
} finally {
setIsLoading(false)
}
}, [])
const changeActiveProperty = useCallback(
async (propertyId: string | null) => {
try {
setIsPending(true)
const result = await setActivePropertyAction(propertyId)
if (result.success) {
// Fetch the updated active property
if (propertyId) {
await fetchActiveProperty()
} else {
setActivePropertyState(null)
}
} else {
console.error(result.error || 'Failed to update active property')
}
} catch (err) {
console.error(err instanceof Error ? err.message : 'An unexpected error occurred')
} finally {
setIsPending(false)
}
},
[fetchActiveProperty],
)
useEffect(() => {
fetchActiveProperty()
}, [fetchActiveProperty])
return {
activeProperty,
isLoading,
error,
setActiveProperty: changeActiveProperty,
isPending,
refetch: fetchActiveProperty,
}
}