mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
feat(ui): introduce LinearBorderContainer component and enhance NotFound pages
- Added LinearBorderContainer component for improved UI styling with gradient borders. - Refactored NotFound components in both web and dashboard to utilize LinearBorderContainer, enhancing visual presentation and user experience. - Updated layout and messaging for 404 error pages to provide clearer navigation options. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -1,22 +1,49 @@
|
||||
import { Button } from '@afilmory/ui'
|
||||
import { Button, LinearBorderContainer } from '@afilmory/ui'
|
||||
import { useLocation, useNavigate } from 'react-router'
|
||||
|
||||
export const NotFound = () => {
|
||||
const location = useLocation()
|
||||
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<div className="prose center dark:prose-invert m-auto size-full flex-col">
|
||||
<main className="flex grow flex-col items-center justify-center">
|
||||
<p className="font-semibold">You have come to a desert of knowledge where there is nothing.</p>
|
||||
<p>
|
||||
Current path: <code>{location.pathname}</code>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<Button onClick={() => navigate('/')}>Back to Home</Button>
|
||||
</p>
|
||||
</main>
|
||||
return (
|
||||
<div className="bg-background text-text relative flex min-h-dvh flex-1 flex-col">
|
||||
<div className="flex flex-1 items-center justify-center px-4 py-10 sm:px-6">
|
||||
<LinearBorderContainer>
|
||||
<div className="relative w-full max-w-[640px] overflow-hidden border border-white/5">
|
||||
{/* Glassmorphic background effects */}
|
||||
<div className="pointer-events-none absolute inset-0 opacity-60">
|
||||
<div className="from-accent/20 absolute -inset-32 bg-linear-to-br via-transparent to-transparent blur-3xl" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.08),transparent_55%)]" />
|
||||
</div>
|
||||
|
||||
<div className="relative p-10 sm:p-12">
|
||||
<div>
|
||||
<p className="text-text-tertiary mb-3 text-xs font-semibold tracking-[0.55em] uppercase">404</p>
|
||||
<h1 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">Page Not Found</h1>
|
||||
<p className="text-text-secondary mb-6 text-base leading-relaxed">
|
||||
The page you're looking for doesn't exist. It may have been moved or deleted. Please check the URL or
|
||||
return to the home page to continue exploring.
|
||||
</p>
|
||||
|
||||
<div className="bg-material-medium/40 border-fill-tertiary mb-6 rounded-2xl border px-5 py-4 text-sm">
|
||||
<p className="text-text-secondary">
|
||||
Current path: <span className="text-text font-medium">{location.pathname}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Button variant="primary" className="glassmorphic-btn flex-1" onClick={() => navigate('/')}>
|
||||
Back to Home
|
||||
</Button>
|
||||
<Button variant="ghost" className="flex-1" onClick={() => navigate(-1)}>
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LinearBorderContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TenantResolutionOptions } from '../modules/tenant/tenant-context-resolver.service'
|
||||
import type { TenantResolutionOptions } from '../modules/platform/tenant/tenant-context-resolver.service'
|
||||
|
||||
export const TENANT_RESOLUTION_OPTIONS = Symbol('core:tenantResolutionOptions')
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export class BuilderSettingService {
|
||||
const tenant = requireTenantContext()
|
||||
const config = await this.builderConfigService.getConfigForTenant(tenant.tenant.id)
|
||||
return {
|
||||
system: config.system,
|
||||
system: this.normalizeSystemSettings(config.system),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,4 +76,31 @@ export class BuilderSettingService {
|
||||
'system.observability.performance.worker.timeout': system.observability.performance.worker.timeout,
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeSystemSettings(system: BuilderConfig['system']): BuilderSystemSettingsDto {
|
||||
const supportedFormats =
|
||||
system.processing.supportedFormats instanceof Set
|
||||
? Array.from(system.processing.supportedFormats)
|
||||
: system.processing.supportedFormats
|
||||
|
||||
return {
|
||||
processing: {
|
||||
...system.processing,
|
||||
digestSuffixLength: system.processing.digestSuffixLength ?? 0,
|
||||
supportedFormats,
|
||||
},
|
||||
observability: {
|
||||
...system.observability,
|
||||
logging: {
|
||||
...system.observability.logging,
|
||||
},
|
||||
performance: {
|
||||
...system.observability.performance,
|
||||
worker: {
|
||||
...system.observability.performance.worker,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
UiGroupNode,
|
||||
UiSchema,
|
||||
UiSectionNode,
|
||||
} from '../ui-schema/ui-schema.type'
|
||||
} from '../../ui/ui-schema/ui-schema.type'
|
||||
import type { DEFAULT_SETTING_DEFINITIONS } from './setting.constant'
|
||||
|
||||
export type SettingDefinition<Schema extends z.ZodTypeAny = z.ZodTypeAny> = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { injectable } from 'tsyringe'
|
||||
|
||||
import type { UiNode } from '../../ui/ui-schema/ui-schema.type'
|
||||
import type { SettingEntryInput } from '../setting/setting.service'
|
||||
import { SettingService } from '../setting/setting.service'
|
||||
import type { UiNode } from '../ui-schema/ui-schema.type'
|
||||
import type { SiteSettingEntryInput, SiteSettingKey, SiteSettingUiSchemaResponse } from './site-setting.type'
|
||||
import { ONBOARDING_SITE_SETTING_KEYS, SITE_SETTING_KEYS } from './site-setting.type'
|
||||
import { SITE_SETTING_UI_SCHEMA, SITE_SETTING_UI_SCHEMA_KEYS } from './site-setting.ui-schema'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { UiSchema } from '../../ui/ui-schema/ui-schema.type'
|
||||
import type { SettingEntryInput } from '../setting/setting.service'
|
||||
import type { SettingKeyType } from '../setting/setting.type'
|
||||
import type { UiSchema } from '../ui-schema/ui-schema.type'
|
||||
|
||||
export const SITE_SETTING_KEYS = [
|
||||
'site.name',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { UiNode, UiSchema } from '../ui-schema/ui-schema.type'
|
||||
import type { UiNode, UiSchema } from '../../ui/ui-schema/ui-schema.type'
|
||||
import type { SiteSettingKey } from './site-setting.type'
|
||||
|
||||
export const SITE_SETTING_UI_SCHEMA_VERSION = '1.0.0'
|
||||
|
||||
@@ -120,7 +120,6 @@ export class FeedService implements OnModuleInit, OnModuleDestroy {
|
||||
url: siteConfig.author.url,
|
||||
avatar: siteConfig.author.avatar ?? null,
|
||||
},
|
||||
feed: siteConfig.feed,
|
||||
}
|
||||
|
||||
const xml = generateRSSFeed(manifest.data, feedConfig)
|
||||
|
||||
@@ -196,7 +196,7 @@ export abstract class StaticAssetService {
|
||||
private extractRelativePath(fullPath: string): string {
|
||||
const index = fullPath.indexOf(this.routeSegment)
|
||||
if (index === -1) {
|
||||
return ''
|
||||
return this.stripLeadingSlashes(fullPath)
|
||||
}
|
||||
|
||||
const sliceStart = index + this.routeSegment.length
|
||||
|
||||
@@ -293,11 +293,12 @@ export class AuthRegistrationService {
|
||||
|
||||
const initialSettings = this.normalizeSettings(settings)
|
||||
if (initialSettings.length > 0 && tenantId) {
|
||||
const scopedTenantId = tenantId
|
||||
await this.settingService.setMany(
|
||||
initialSettings.map((entry) => ({
|
||||
...entry,
|
||||
options: {
|
||||
tenantId,
|
||||
tenantId: scopedTenantId,
|
||||
isSensitive: false,
|
||||
},
|
||||
})),
|
||||
@@ -335,6 +336,9 @@ export class AuthRegistrationService {
|
||||
if (!sessionUserId) {
|
||||
throw new BizException(ErrorCode.AUTH_UNAUTHORIZED, { message: '当前登录状态无效,请重新登录。' })
|
||||
}
|
||||
if (!sessionUser) {
|
||||
throw new BizException(ErrorCode.AUTH_UNAUTHORIZED, { message: '无法获取当前用户信息,请重新登录。' })
|
||||
}
|
||||
|
||||
const db = this.dbAccessor.get()
|
||||
const [record] = await db
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@afilmory/ui'
|
||||
import { Button, LinearBorderContainer } from '@afilmory/ui'
|
||||
import { useLocation, useNavigate } from 'react-router'
|
||||
|
||||
export function NotFound() {
|
||||
@@ -6,88 +6,43 @@ export function NotFound() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
{/* Header spacer */}
|
||||
<div className="h-16" />
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex flex-1 items-center justify-center px-6">
|
||||
<div className="w-full max-w-lg">
|
||||
{/* 404 icon and status */}
|
||||
<div className="mb-8 text-center">
|
||||
<div className="bg-background-secondary mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full">
|
||||
<svg
|
||||
className="text-blue h-8 w-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"
|
||||
/>
|
||||
</svg>
|
||||
<div className="relative flex min-h-dvh flex-1 flex-col bg-background text-text">
|
||||
<div className="flex flex-1 items-center justify-center px-4 py-10 sm:px-6">
|
||||
<LinearBorderContainer>
|
||||
<div className="relative w-full max-w-[640px] overflow-hidden border border-white/5">
|
||||
{/* Glassmorphic background effects */}
|
||||
<div className="pointer-events-none absolute inset-0 opacity-60">
|
||||
<div className="absolute -inset-32 bg-linear-to-br from-accent/20 via-transparent to-transparent blur-3xl" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.08),transparent_55%)]" />
|
||||
</div>
|
||||
<h1 className="text-text mb-2 text-6xl font-bold">404</h1>
|
||||
<h2 className="text-text mb-2 text-2xl font-medium">Page not found</h2>
|
||||
<p className="text-text-secondary text-lg">The page you're looking for doesn't exist</p>
|
||||
</div>
|
||||
|
||||
{/* Current path info */}
|
||||
<div className="bg-material-medium border-fill-tertiary mb-8 rounded-lg border p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg
|
||||
className="text-text-tertiary mt-0.5 h-5 w-5 flex-shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-text-secondary mb-1 text-sm">Requested URL</p>
|
||||
<code className="text-text bg-material-thin rounded px-2 py-1 font-mono text-sm break-all">
|
||||
{location.pathname}
|
||||
</code>
|
||||
<div className="relative p-10 sm:p-12">
|
||||
<div>
|
||||
<p className="text-text-tertiary mb-3 text-xs font-semibold uppercase tracking-[0.55em]">404</p>
|
||||
<h1 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">Page Not Found</h1>
|
||||
<p className="text-text-secondary mb-6 text-base leading-relaxed">
|
||||
The page you're looking for doesn't exist. It may have been moved or deleted. Please check the URL or
|
||||
navigate back to continue using the dashboard.
|
||||
</p>
|
||||
|
||||
<div className="bg-material-medium/40 border-fill-tertiary mb-6 rounded-2xl border px-5 py-4 text-sm">
|
||||
<p className="text-text-secondary">
|
||||
Requested URL: <span className="text-text font-medium">{location.pathname}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Button variant="primary" className="glassmorphic-btn flex-1" onClick={() => navigate('/')}>
|
||||
Go Home
|
||||
</Button>
|
||||
<Button variant="ghost" className="flex-1" onClick={() => navigate(-1)}>
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="mb-8 flex flex-col gap-3 sm:flex-row">
|
||||
<Button
|
||||
onClick={() => navigate('/')}
|
||||
className="bg-material-opaque text-text-vibrant hover:bg-control-enabled/90 h-10 flex-1 border-0 font-medium transition-colors"
|
||||
>
|
||||
Go Home
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate(-1)}
|
||||
className="bg-material-thin text-text border-fill-tertiary hover:bg-fill-tertiary h-10 flex-1 border font-medium transition-colors"
|
||||
>
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Help text */}
|
||||
<div className="text-center">
|
||||
<p className="text-text-secondary text-sm">
|
||||
If you think this is a mistake, please check the URL or contact support.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex h-16 items-center justify-center">
|
||||
<p className="text-text-secondary/50 text-xs">Error 404 • Page not found</p>
|
||||
</LinearBorderContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { clsxm } from '@afilmory/utils'
|
||||
import { ChevronDown, LogOut, Settings, User as UserIcon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import { usePageRedirect } from '~/hooks/usePageRedirect'
|
||||
import type { BetterAuthUser } from '~/modules/auth/types'
|
||||
@@ -89,11 +90,11 @@ export function UserMenu({ user }: UserMenuProps) {
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem icon={<UserIcon className="size-4" />}>
|
||||
<a href="/settings/account">Account Settings</a>
|
||||
<Link to="/settings/account">Account Settings</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem icon={<Settings className="size-4" />}>
|
||||
<a href="/settings/site">Preferences</a>
|
||||
<Link to="/settings/site">Preferences</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ScrollArea } from '@afilmory/ui'
|
||||
import { LinearBorderContainer, ScrollArea } from '@afilmory/ui'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type { FC, KeyboardEvent } from 'react'
|
||||
@@ -13,7 +13,6 @@ import { useRegistrationForm } from '~/modules/auth/hooks/useRegistrationForm'
|
||||
import { getTenantSlugFromHost } from '~/modules/auth/utils/domain'
|
||||
import type { SchemaFormValue, UiSchema } from '~/modules/schema-form/types'
|
||||
import { getWelcomeSiteSchema } from '~/modules/welcome/api'
|
||||
import { LinearBorderContainer } from '~/modules/welcome/components/LinearBorderContainer'
|
||||
import { DEFAULT_SITE_SETTINGS_VALUES, SITE_SETTINGS_KEYS, siteSettingsSchema } from '~/modules/welcome/siteSchema'
|
||||
import {
|
||||
coerceSiteFieldValue,
|
||||
|
||||
@@ -47,7 +47,7 @@ export function useRegisterTenant() {
|
||||
},
|
||||
onSuccess: ({ slug }) => {
|
||||
try {
|
||||
const loginUrl = buildTenantUrl(slug, '/login')
|
||||
const loginUrl = buildTenantUrl(slug, '/platform/login')
|
||||
setErrorMessage(null)
|
||||
window.location.replace(loginUrl)
|
||||
} catch (redirectError) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Button } from '@afilmory/ui'
|
||||
import { Button, LinearBorderContainer } from '@afilmory/ui'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { LinearBorderContainer } from './LinearBorderContainer'
|
||||
import { buildHomeUrl, buildRegistrationUrl, getCurrentHostname } from './tenant-utils'
|
||||
|
||||
export const TenantMissingStandalone = () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Button } from '@afilmory/ui'
|
||||
import { Button, LinearBorderContainer } from '@afilmory/ui'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { LinearBorderContainer } from './LinearBorderContainer'
|
||||
import { buildHomeUrl, buildRegistrationUrl, getCurrentHostname } from './tenant-utils'
|
||||
|
||||
export const TenantRestrictedStandalone = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Input, Label } from '@afilmory/ui'
|
||||
import { Button, Input, Label, LinearBorderContainer } from '@afilmory/ui'
|
||||
import { Spring } from '@afilmory/utils'
|
||||
import { m } from 'motion/react'
|
||||
import { useMemo, useState } from 'react'
|
||||
@@ -6,7 +6,6 @@ import { useMemo, useState } from 'react'
|
||||
import { SocialAuthButtons } from '~/modules/auth/components/SocialAuthButtons'
|
||||
import { useLogin } from '~/modules/auth/hooks/useLogin'
|
||||
import { getTenantSlugFromHost } from '~/modules/auth/utils/domain'
|
||||
import { LinearBorderContainer } from '~/modules/welcome/components/LinearBorderContainer'
|
||||
|
||||
export function Component() {
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Input, Label } from '@afilmory/ui'
|
||||
import { Button, Input, Label, LinearBorderContainer } from '@afilmory/ui'
|
||||
import { Spring } from '@afilmory/utils'
|
||||
import { m } from 'motion/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
@@ -6,7 +6,6 @@ import { Link } from 'react-router'
|
||||
|
||||
import { useLogin } from '~/modules/auth/hooks/useLogin'
|
||||
import { buildRootTenantUrl, getTenantSlugFromHost } from '~/modules/auth/utils/domain'
|
||||
import { LinearBorderContainer } from '~/modules/welcome/components/LinearBorderContainer'
|
||||
|
||||
export function Component() {
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { Button } from '@afilmory/ui'
|
||||
import { Button, LinearBorderContainer } from '@afilmory/ui'
|
||||
import { Spring } from '@afilmory/utils'
|
||||
import { m } from 'motion/react'
|
||||
import { useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
import { LinearBorderContainer } from '~/modules/welcome/components/LinearBorderContainer'
|
||||
|
||||
function getCurrentHostname() {
|
||||
if (typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return window.location.hostname
|
||||
} catch {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@afilmory/ui'
|
||||
import { Button, LinearBorderContainer } from '@afilmory/ui'
|
||||
import { Spring } from '@afilmory/utils'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { m } from 'motion/react'
|
||||
@@ -11,7 +11,6 @@ import { useSetAuthUser } from '~/atoms/auth'
|
||||
import { ROUTE_PATHS } from '~/constants/routes'
|
||||
import { AUTH_SESSION_QUERY_KEY } from '~/modules/auth/api/session'
|
||||
import { signOutBySource } from '~/modules/auth/auth-client'
|
||||
import { LinearBorderContainer } from '~/modules/welcome/components/LinearBorderContainer'
|
||||
|
||||
export const Component: FC = () => {
|
||||
const location = useLocation()
|
||||
|
||||
79
docs/TENANT_FLOW.md
Normal file
79
docs/TENANT_FLOW.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Tenant & OAuth Flow
|
||||
|
||||
This document describes how tenant resolution, Better Auth instances, and dashboard redirects tie together across the platform.
|
||||
|
||||
## Request Bootstrap
|
||||
|
||||
1. `RequestContextMiddleware` runs first on every request.
|
||||
- Calls `TenantContextResolver.resolve()` to populate `HttpContext.tenant`.
|
||||
- Calls `AuthProvider.getAuth()` so downstream handlers reuse the tenant-aware Better Auth instance.
|
||||
2. `TenantContextResolver` inspects `x-forwarded-host`, `origin`, and `host` headers.
|
||||
- Extracts a slug via `tenant-host.utils.ts`.
|
||||
- Loads the tenant aggregate; if none exists, falls back to the placeholder tenant.
|
||||
- Always stores the original `requestedSlug` (even when placeholder), echoes headers `x-tenant-id` and `x-tenant-slug` (effective slug).
|
||||
|
||||
## Auth Provider
|
||||
|
||||
- `AuthProvider` caches Better Auth instances by `protocol://host::slug::settings-hash`.
|
||||
- The slug priority is:
|
||||
1. `HttpContext.tenant.requestedSlug`
|
||||
2. `HttpContext.tenant.slug`
|
||||
3. Derived from the host (when the context slug is still the placeholder).
|
||||
- Redirect URIs are always built as `<OAuthGateway>/api/auth/callback/:provider?tenantSlug=...`.
|
||||
- Because the requested slug participates in the cache key, the same Better Auth instance handles both the `/auth/social` request and the gateway callback, preserving OAuth state.
|
||||
|
||||
## System Settings & Gateway
|
||||
|
||||
- System settings only manage:
|
||||
- Registration flags
|
||||
- Base domain
|
||||
- OAuth gateway URL
|
||||
- Provider credentials
|
||||
- No per-provider redirect URIs are stored; every provider points to the centralized gateway.
|
||||
- `/auth/social/providers` reflects the enabled providers for the UI.
|
||||
|
||||
## Session Payload
|
||||
|
||||
`GET /auth/session` returns:
|
||||
|
||||
```ts
|
||||
{
|
||||
user: BetterAuthUser,
|
||||
session: BetterAuthSession,
|
||||
tenant: {
|
||||
id: string,
|
||||
slug: string | null, // Effective slug (requested slug if present, otherwise actual)
|
||||
isPlaceholder: boolean
|
||||
} | null
|
||||
}
|
||||
```
|
||||
|
||||
- When the resolver falls back to the placeholder tenant, `tenant.slug` still holds the requested subdomain, and `isPlaceholder` is `true`.
|
||||
- Consumers simply check `tenant.isPlaceholder` to know whether they are in onboarding.
|
||||
|
||||
## Dashboard Behavior
|
||||
|
||||
- **Welcome flow** (`/platform/welcome`):
|
||||
- Locks the slug input to `window.location.hostname` via `getTenantSlugFromHost`.
|
||||
- Shows `TenantMissing` or `TenantRestricted` pages, but relies on backend redirects for actual auth.
|
||||
- **Hooks (`usePageRedirect`)**:
|
||||
- If `tenant` is null or `isPlaceholder`, stay on welcome routes.
|
||||
- If `tenant.slug` exists and differs from the current host, sign out placeholder cookies and `window.location.replace(buildTenantUrl(tenant.slug))`.
|
||||
- Superadmin routes are gated separately.
|
||||
|
||||
## OAuth Happy Path
|
||||
|
||||
1. User opens `https://slug.example.com` (maybe not provisioned yet).
|
||||
2. Resolver sets `requestedSlug = "slug"`, but tenant aggregate may still be the placeholder.
|
||||
3. User clicks “Sign in with GitHub” → `/auth/social` uses `requestedSlug` and redirects via the OAuth gateway.
|
||||
4. Gateway forwards the callback to `https://slug.example.com/api/auth/callback/github`.
|
||||
5. Resolver again sets `requestedSlug = "slug"`; Better Auth instance cache hits, so `state` matches.
|
||||
6. `/auth/session` returns `{ tenant: { slug: "slug", isPlaceholder: true } }` → dashboard stays on welcome, no cross-subdomain jump.
|
||||
7. Once the tenant is provisioned, future sessions have `isPlaceholder: false`, and `usePageRedirect` ensures we land on the actual workspace subdomain.
|
||||
|
||||
## Key Guarantees
|
||||
|
||||
- Only a single `tenant.slug` crosses the API boundary; there are no ambiguous fields.
|
||||
- Placeholder detection is a boolean (`isPlaceholder`).
|
||||
- Better Auth instances survive OAuth handshakes regardless of tenant provisioning state.
|
||||
- Headers `x-tenant-id` / `x-tenant-slug` always mirror the effective slug, so backend services and the dashboard remain consistent.
|
||||
@@ -7,29 +7,36 @@ type LinearBorderContainerProps = {
|
||||
|
||||
/**
|
||||
* Color tint for the border gradient.
|
||||
* @default 'text' - Uses the default text color
|
||||
* @example 'accent' - Uses the accent color
|
||||
* @example 'red' - Uses the red system color
|
||||
* @default 'var(--color-text-secondary)' - Uses the default text secondary color
|
||||
* @example 'var(--color-accent)' - Uses the accent color
|
||||
* @example 'var(--color-red)' - Uses the red system color
|
||||
*/
|
||||
tint?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A container with linear gradient borders on all sides.
|
||||
* Creates a sophisticated border effect using CSS gradients.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <LinearBorderContainer className="bg-background-tertiary">
|
||||
* <div className="grid lg:grid-cols-[280px_1fr]">
|
||||
* <Sidebar />
|
||||
* <Content />
|
||||
* <LinearBorderContainer className="bg-background-tertiary">
|
||||
* <div className="p-12">
|
||||
* <h1>Content with linear gradient borders</h1>
|
||||
* </div>
|
||||
* </LinearBorderContainer>
|
||||
* ```
|
||||
*
|
||||
* @example With custom tint
|
||||
* ```tsx
|
||||
* <LinearBorderContainer tint="var(--color-accent)">
|
||||
* <div>Accent-colored borders</div>
|
||||
* </LinearBorderContainer>
|
||||
* ```
|
||||
*/
|
||||
export const LinearBorderContainer: FC<LinearBorderContainerProps> = ({
|
||||
children,
|
||||
className,
|
||||
|
||||
tint = 'var(--color-text-secondary)',
|
||||
}) => {
|
||||
// Generate inline styles for gradients with dynamic tint color
|
||||
@@ -40,7 +47,6 @@ export const LinearBorderContainer: FC<LinearBorderContainerProps> = ({
|
||||
background: `linear-gradient(to bottom, transparent -15%, ${tint} 50%, transparent 115%)`,
|
||||
}
|
||||
|
||||
// Advanced mode: uses flex layout for borders that span full dimensions
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className={clsxm('flex flex-row', className)}>
|
||||
@@ -53,13 +59,13 @@ export const LinearBorderContainer: FC<LinearBorderContainerProps> = ({
|
||||
{/* Main content area */}
|
||||
{children}
|
||||
|
||||
{/* Right border container */}
|
||||
{/* Right border */}
|
||||
<div className="flex shrink-0 flex-col">
|
||||
<div className="absolute top-0 bottom-0 z-1 w-[0.5px]" style={verticalGradient} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom border container */}
|
||||
{/* Bottom border */}
|
||||
<div className="w-[2px] shrink-0">
|
||||
<div className="absolute right-0 left-0 z-1 h-[0.5px]" style={horizontalGradient} />
|
||||
</div>
|
||||
1
packages/ui/src/container/index.ts
Normal file
1
packages/ui/src/container/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { LinearBorderContainer } from './LinearBorderContainer'
|
||||
@@ -2,6 +2,7 @@ export * from './button'
|
||||
export * from './checkbox'
|
||||
export * from './checkbox'
|
||||
export * from './collapsible'
|
||||
export * from './container'
|
||||
export * from './context-menu'
|
||||
export * from './dialog'
|
||||
export * from './divider'
|
||||
|
||||
Reference in New Issue
Block a user