mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
feat: add Gallery Showcase component and update documentation
- Introduced a new Gallery Showcase component to display featured galleries on the landing page. - Updated localization files to include new strings for the Gallery Showcase. - Enhanced the landing page by integrating the Gallery Showcase component alongside existing elements. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -224,7 +224,7 @@
|
||||
"title": "Cloudflare Pages",
|
||||
"meta": {
|
||||
"title": "Cloudflare Pages",
|
||||
"description": "Deploy your gallery to Cloudflare Pages for fast global CDN distribution.",
|
||||
"description": "Guide to deploying Afilmory via Cloudflare Pages.",
|
||||
"createdAt": "2025-07-20T22:35:03+08:00",
|
||||
"lastModified": "2025-11-23T19:40:52+08:00",
|
||||
"order": "53"
|
||||
|
||||
@@ -282,7 +282,7 @@ export const routes: RouteConfig[] = [
|
||||
title: 'Cloudflare Pages',
|
||||
meta: {
|
||||
title: 'Cloudflare Pages',
|
||||
description: 'Deploy your gallery to Cloudflare Pages for fast global CDN distribution.',
|
||||
description: 'Guide to deploying Afilmory via Cloudflare Pages.',
|
||||
createdAt: '2025-07-20T22:35:03+08:00',
|
||||
lastModified: '2025-11-23T19:40:52+08:00',
|
||||
order: '53',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { GalleryShowcase } from '~/components/landing'
|
||||
import {
|
||||
CreateSpaceCTA,
|
||||
NocturneHero,
|
||||
@@ -11,6 +12,7 @@ export default function Home() {
|
||||
<main className="relative z-10 mx-auto flex w-full max-w-6xl flex-col gap-20 px-4 py-16 sm:px-6 lg:px-0">
|
||||
<NocturneHero />
|
||||
<CreateSpaceCTA />
|
||||
<GalleryShowcase />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
227
apps/landing/src/components/landing/GalleryShowcase.tsx
Normal file
227
apps/landing/src/components/landing/GalleryShowcase.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { API_URL } from '~/constants/env'
|
||||
|
||||
interface FeaturedGalleryAuthor {
|
||||
name: string
|
||||
avatar: string | null
|
||||
}
|
||||
|
||||
interface FeaturedGallery {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
domain: string | null
|
||||
description: string | null
|
||||
author: FeaturedGalleryAuthor | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface FeaturedGalleriesResponse {
|
||||
galleries: FeaturedGallery[]
|
||||
}
|
||||
|
||||
const API_BASE_URL = API_URL.replace(/\/$/, '')
|
||||
const FEATURED_GALLERIES_ENDPOINT = `${API_BASE_URL || ''}/featured-galleries`
|
||||
|
||||
async function fetchFeaturedGalleries(): Promise<FeaturedGalleriesResponse> {
|
||||
const response = await fetch(FEATURED_GALLERIES_ENDPOINT, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch featured galleries')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export const GalleryShowcase = () => {
|
||||
const t = useTranslations('GalleryShowcase')
|
||||
const locale = useLocale()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
const { data, isLoading, error } = useQuery<FeaturedGalleriesResponse>({
|
||||
queryKey: ['featured-galleries'],
|
||||
queryFn: fetchFeaturedGalleries,
|
||||
enabled: mounted,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
})
|
||||
|
||||
const galleries = data?.galleries ?? []
|
||||
|
||||
const getBaseDomain = () => {
|
||||
if (typeof window === 'undefined') return 'afilmory.art'
|
||||
const { hostname } = window.location
|
||||
// Extract base domain from current hostname, or use default
|
||||
if (
|
||||
hostname.includes('.') &&
|
||||
!hostname.includes('localhost') &&
|
||||
!hostname.includes('127.0.0.1')
|
||||
) {
|
||||
return hostname.split('.').slice(-2).join('.')
|
||||
}
|
||||
return 'afilmory.art'
|
||||
}
|
||||
|
||||
const buildGalleryUrl = (gallery: FeaturedGallery) => {
|
||||
if (typeof window === 'undefined') return '#'
|
||||
const { protocol } = window.location
|
||||
// Prefer custom domain, fallback to slug subdomain
|
||||
if (gallery.domain) {
|
||||
return `${protocol}//${gallery.domain}`
|
||||
}
|
||||
const baseDomain = getBaseDomain()
|
||||
return `${protocol}//${gallery.slug}.${baseDomain}`
|
||||
}
|
||||
|
||||
const getDisplayUrl = (gallery: FeaturedGallery) => {
|
||||
// Prefer custom domain, fallback to slug subdomain
|
||||
if (gallery.domain) {
|
||||
return gallery.domain
|
||||
}
|
||||
return `${gallery.slug}.${getBaseDomain()}`
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
const localeMap: Record<string, string> = {
|
||||
'zh-CN': 'zh-CN',
|
||||
'zh-TW': 'zh-TW',
|
||||
'zh-HK': 'zh-HK',
|
||||
en: 'en-US',
|
||||
jp: 'ja-JP',
|
||||
ko: 'ko-KR',
|
||||
}
|
||||
const dateLocale = localeMap[locale] || 'en-US'
|
||||
return date.toLocaleDateString(dateLocale, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="gallery-showcase" className="space-y-10">
|
||||
<div className="text-center">
|
||||
<p className="text-xs tracking-[0.6em] text-white/40 uppercase">
|
||||
{t('eyebrow')}
|
||||
</p>
|
||||
<h2 className="mt-4 font-serif text-3xl text-white">{t('title')}</h2>
|
||||
<p className="mt-3 text-base text-white/70">{t('description')}</p>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-32 animate-pulse rounded-3xl border border-white/10 bg-white/5"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-3xl border border-white/10 bg-white/5 p-6 text-center text-white/60">
|
||||
{t('error')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && galleries.length === 0 && (
|
||||
<div className="rounded-3xl border border-white/10 bg-white/5 p-6 text-center text-white/60">
|
||||
{t('empty')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && galleries.length > 0 && (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{galleries.map((gallery) => (
|
||||
<a
|
||||
key={gallery.id}
|
||||
href={buildGalleryUrl(gallery)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group relative overflow-hidden rounded-3xl border border-white/10 bg-linear-to-br from-white/8 to-transparent p-6 transition hover:border-white/30 hover:bg-white/10"
|
||||
>
|
||||
{/* Author Avatar & Info */}
|
||||
{gallery.author && (
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="relative size-10 shrink-0 overflow-hidden rounded-full border border-white/10 bg-white/5">
|
||||
{gallery.author.avatar ? (
|
||||
<img
|
||||
src={gallery.author.avatar}
|
||||
alt={gallery.author.name}
|
||||
className="h-full w-full object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{(!gallery.author.avatar ||
|
||||
gallery.author.avatar === '') && (
|
||||
<div className="bg-accent-20 text-accent flex h-full w-full items-center justify-center">
|
||||
<span className="text-sm font-medium">
|
||||
{gallery.author.name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-white">
|
||||
{gallery.author.name}
|
||||
</p>
|
||||
<p className="truncate text-xs text-white/50">
|
||||
{getDisplayUrl(gallery)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Site Name */}
|
||||
<h3 className="group-hover:text-accent mb-2 font-serif text-xl text-white transition">
|
||||
{gallery.name}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
{gallery.description && (
|
||||
<p className="mb-4 line-clamp-2 text-sm leading-relaxed text-white/70">
|
||||
{gallery.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<div className="mb-4 h-px w-full bg-linear-to-r from-transparent via-white/30 to-transparent opacity-50" />
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-white/40">
|
||||
{formatDate(gallery.createdAt)}
|
||||
</div>
|
||||
<div className="text-white/30 transition group-hover:text-white/60">
|
||||
<i className="i-lucide-external-link size-4" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
export { CreateSpaceModal } from './CreateSpaceModal'
|
||||
export { GalleryShowcase } from './GalleryShowcase'
|
||||
export { NocturneBackground } from './NocturneBackground'
|
||||
export { NocturneButton } from './NocturneButton'
|
||||
export * from './NocturneSections'
|
||||
|
||||
12
apps/landing/src/lib/query-client.ts
Normal file
12
apps/landing/src/lib/query-client.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime)
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -177,5 +177,12 @@
|
||||
"privacy": "Privacy Policy",
|
||||
"terms": "Terms of Service",
|
||||
"copyright": "© 2025 Afilmory.art"
|
||||
},
|
||||
"GalleryShowcase": {
|
||||
"eyebrow": "Registered Galleries",
|
||||
"title": "Explore registered visual spaces",
|
||||
"description": "Discover amazing photography archives created by other photographers and curators, experience different visual storytelling styles.",
|
||||
"error": "Failed to load gallery list, please try again later.",
|
||||
"empty": "No registered galleries yet."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,5 +177,12 @@
|
||||
"privacy": "隐私政策",
|
||||
"terms": "服务条款",
|
||||
"copyright": "© 2025 Afilmory.art"
|
||||
},
|
||||
"GalleryShowcase": {
|
||||
"eyebrow": "已注册画展",
|
||||
"title": "探索已注册的影像空间",
|
||||
"description": "发现其他摄影师和策展人创建的精彩影像档案馆,感受不同的视觉叙事风格。",
|
||||
"error": "加载画展列表时出错,请稍后重试。",
|
||||
"empty": "暂无已注册的画展。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { LazyMotion } from 'motion/react'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import type { JSX, PropsWithChildren } from 'react'
|
||||
|
||||
import { ProviderComposer } from '../../components/common/ProviderComposer'
|
||||
import { queryClient } from '../../lib/query-client'
|
||||
|
||||
const loadFeatures = () =>
|
||||
import('./framer-lazy-feature').then((res) => res.default)
|
||||
const contexts: JSX.Element[] = [
|
||||
<ThemeProvider key="themeProvider" />,
|
||||
|
||||
<QueryClientProvider key="queryClient" client={queryClient} />,
|
||||
<LazyMotion features={loadFeatures} strict key="framer" />,
|
||||
]
|
||||
export function Providers({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<>
|
||||
<ProviderComposer contexts={contexts}>{children}</ProviderComposer>
|
||||
</>
|
||||
)
|
||||
return <ProviderComposer contexts={contexts}>{children}</ProviderComposer>
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { AuthModule } from './platform/auth/auth.module'
|
||||
import { BillingModule } from './platform/billing/billing.module'
|
||||
import { DashboardModule } from './platform/dashboard/dashboard.module'
|
||||
import { DataManagementModule } from './platform/data-management/data-management.module'
|
||||
import { FeaturedGalleriesModule } from './platform/featured-galleries/featured-galleries.module'
|
||||
import { SuperAdminModule } from './platform/super-admin/super-admin.module'
|
||||
import { TenantModule } from './platform/tenant/tenant.module'
|
||||
|
||||
@@ -62,6 +63,7 @@ function createEventModuleOptions(redis: RedisAccessor) {
|
||||
BillingModule,
|
||||
DataManagementModule,
|
||||
TenantModule,
|
||||
FeaturedGalleriesModule,
|
||||
DataSyncModule,
|
||||
FeedModule,
|
||||
OgModule,
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
import {
|
||||
authSessions,
|
||||
authUsers,
|
||||
photoAssets,
|
||||
reactions,
|
||||
settings,
|
||||
tenantAuthAccounts,
|
||||
tenantAuthSessions,
|
||||
tenantAuthUsers,
|
||||
tenants,
|
||||
} from '@afilmory/db'
|
||||
import { authSessions, authUsers, photoAssets, reactions, settings, tenants } from '@afilmory/db'
|
||||
import { EventEmitterService } from '@afilmory/framework'
|
||||
import { DbAccessor } from 'core/database/database.provider'
|
||||
import { BizException, ErrorCode } from 'core/errors'
|
||||
@@ -76,9 +66,7 @@ export class DataManagementService {
|
||||
await tx.delete(photoAssets).where(eq(photoAssets.tenantId, tenantId))
|
||||
await tx.delete(reactions).where(eq(reactions.tenantId, tenantId))
|
||||
await tx.delete(settings).where(eq(settings.tenantId, tenantId))
|
||||
await tx.delete(tenantAuthAccounts).where(eq(tenantAuthAccounts.tenantId, tenantId))
|
||||
await tx.delete(tenantAuthSessions).where(eq(tenantAuthSessions.tenantId, tenantId))
|
||||
await tx.delete(tenantAuthUsers).where(eq(tenantAuthUsers.tenantId, tenantId))
|
||||
|
||||
await tx.delete(authSessions).where(eq(authSessions.tenantId, tenantId))
|
||||
await tx.update(authUsers).set({ tenantId: null, role: 'user' }).where(eq(authUsers.tenantId, tenantId))
|
||||
await tx.delete(tenants).where(eq(tenants.id, tenantId))
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Controller, Get } from '@afilmory/framework'
|
||||
import { AllowPlaceholderTenant } from 'core/decorators/allow-placeholder.decorator'
|
||||
import { SkipTenantGuard } from 'core/decorators/skip-tenant.decorator'
|
||||
import { BypassResponseTransform } from 'core/interceptors/response-transform.decorator'
|
||||
|
||||
import { FeaturedGalleriesService } from './featured-galleries.service'
|
||||
|
||||
@Controller('featured-galleries')
|
||||
@SkipTenantGuard()
|
||||
@BypassResponseTransform()
|
||||
export class FeaturedGalleriesController {
|
||||
constructor(private readonly featuredGalleriesService: FeaturedGalleriesService) {}
|
||||
|
||||
@AllowPlaceholderTenant()
|
||||
@Get('/')
|
||||
async listFeaturedGalleries() {
|
||||
return await this.featuredGalleriesService.listFeaturedGalleries()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@afilmory/framework'
|
||||
import { DatabaseModule } from 'core/database/database.module'
|
||||
|
||||
import { TenantModule } from '../tenant/tenant.module'
|
||||
import { FeaturedGalleriesController } from './featured-galleries.controller'
|
||||
import { FeaturedGalleriesService } from './featured-galleries.service'
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, TenantModule],
|
||||
controllers: [FeaturedGalleriesController],
|
||||
providers: [FeaturedGalleriesService],
|
||||
})
|
||||
export class FeaturedGalleriesModule {}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { authUsers, settings, tenantDomains } from '@afilmory/db'
|
||||
import { DbAccessor } from 'core/database/database.provider'
|
||||
import { normalizeDate } from 'core/helpers/normalize.helper'
|
||||
import { and, asc, eq, inArray, sql } from 'drizzle-orm'
|
||||
import { injectable } from 'tsyringe'
|
||||
|
||||
import { TenantService } from '../tenant/tenant.service'
|
||||
|
||||
@injectable()
|
||||
export class FeaturedGalleriesService {
|
||||
constructor(
|
||||
private readonly tenantService: TenantService,
|
||||
private readonly dbAccessor: DbAccessor,
|
||||
) {}
|
||||
|
||||
async listFeaturedGalleries() {
|
||||
const aggregates = await this.tenantService.listTenants()
|
||||
|
||||
// Filter out banned, inactive, and suspended tenants
|
||||
const validTenants = aggregates
|
||||
.filter((aggregate) => {
|
||||
const { tenant } = aggregate
|
||||
return !tenant.banned && tenant.status === 'active' && tenant.slug !== 'root' && tenant.slug !== 'placeholder'
|
||||
})
|
||||
.slice(0, 20) // Limit to 20 most recent
|
||||
|
||||
const tenantIds = validTenants.map((aggregate) => aggregate.tenant.id)
|
||||
if (tenantIds.length === 0) {
|
||||
return { galleries: [] }
|
||||
}
|
||||
|
||||
const db = this.dbAccessor.get()
|
||||
|
||||
// Fetch site settings for all tenants
|
||||
const siteSettings = await db
|
||||
.select()
|
||||
.from(settings)
|
||||
.where(and(inArray(settings.tenantId, tenantIds), inArray(settings.key, ['site.name', 'site.description'])))
|
||||
|
||||
// Fetch primary author (admin) for each tenant
|
||||
const authors = await db
|
||||
.select({
|
||||
tenantId: authUsers.tenantId,
|
||||
name: authUsers.name,
|
||||
image: authUsers.image,
|
||||
})
|
||||
.from(authUsers)
|
||||
.where(inArray(authUsers.tenantId, tenantIds))
|
||||
.orderBy(
|
||||
sql`case when ${authUsers.role} = 'admin' then 0 when ${authUsers.role} = 'superadmin' then 1 else 2 end`,
|
||||
asc(authUsers.createdAt),
|
||||
)
|
||||
|
||||
// Fetch verified domains for all tenants
|
||||
const domains = await db
|
||||
.select({
|
||||
tenantId: tenantDomains.tenantId,
|
||||
domain: tenantDomains.domain,
|
||||
})
|
||||
.from(tenantDomains)
|
||||
.where(and(inArray(tenantDomains.tenantId, tenantIds), eq(tenantDomains.status, 'verified')))
|
||||
|
||||
// Build maps for quick lookup
|
||||
const settingsMap = new Map<string, Map<string, string | null>>()
|
||||
for (const setting of siteSettings) {
|
||||
if (!settingsMap.has(setting.tenantId)) {
|
||||
settingsMap.set(setting.tenantId, new Map())
|
||||
}
|
||||
settingsMap.get(setting.tenantId)!.set(setting.key, setting.value)
|
||||
}
|
||||
|
||||
const authorMap = new Map<string, { name: string; avatar: string | null }>()
|
||||
for (const author of authors) {
|
||||
if (!authorMap.has(author.tenantId!)) {
|
||||
authorMap.set(author.tenantId!, {
|
||||
name: author.name,
|
||||
avatar: author.image ?? null,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const domainMap = new Map<string, string>()
|
||||
for (const domain of domains) {
|
||||
// Use the first verified domain for each tenant
|
||||
if (!domainMap.has(domain.tenantId)) {
|
||||
domainMap.set(domain.tenantId, domain.domain)
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
const featuredGalleries = validTenants.map((aggregate) => {
|
||||
const { tenant } = aggregate
|
||||
const tenantSettings = settingsMap.get(tenant.id) ?? new Map()
|
||||
const author = authorMap.get(tenant.id)
|
||||
const domain = domainMap.get(tenant.id)
|
||||
|
||||
return {
|
||||
id: tenant.id,
|
||||
name: tenantSettings.get('site.name') ?? tenant.name,
|
||||
slug: tenant.slug,
|
||||
domain: domain ?? null,
|
||||
description: tenantSettings.get('site.description') ?? null,
|
||||
author: author
|
||||
? {
|
||||
name: author.name,
|
||||
avatar: author.avatar,
|
||||
}
|
||||
: null,
|
||||
createdAt: normalizeDate(tenant.createdAt) ?? tenant.createdAt,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
galleries: featuredGalleries,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,67 +168,6 @@ export const creemSubscriptions = pgTable('creem_subscription', {
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
})
|
||||
|
||||
export const tenantAuthUsers = pgTable(
|
||||
'tenant_auth_user',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
tenantId: text('tenant_id')
|
||||
.notNull()
|
||||
.references(() => tenants.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull(),
|
||||
email: text('email').notNull(),
|
||||
emailVerified: boolean('email_verified').default(false).notNull(),
|
||||
image: text('image'),
|
||||
role: text('role').default('guest').notNull(),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
twoFactorEnabled: boolean('two_factor_enabled').default(false).notNull(),
|
||||
username: text('username'),
|
||||
displayUsername: text('display_username'),
|
||||
banned: boolean('banned').default(false).notNull(),
|
||||
banReason: text('ban_reason'),
|
||||
banExpires: timestamp('ban_expires_at', { mode: 'string' }),
|
||||
},
|
||||
(t) => [unique('uq_tenant_auth_user_tenant_email').on(t.tenantId, t.email)],
|
||||
)
|
||||
|
||||
export const tenantAuthSessions = pgTable('tenant_auth_session', {
|
||||
id: text('id').primaryKey(),
|
||||
tenantId: text('tenant_id')
|
||||
.notNull()
|
||||
.references(() => tenants.id, { onDelete: 'cascade' }),
|
||||
expiresAt: timestamp('expires_at', { mode: 'string' }).notNull(),
|
||||
token: text('token').notNull().unique(),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => tenantAuthUsers.id, { onDelete: 'cascade' }),
|
||||
})
|
||||
|
||||
export const tenantAuthAccounts = pgTable('tenant_auth_account', {
|
||||
id: text('id').primaryKey(),
|
||||
tenantId: text('tenant_id')
|
||||
.notNull()
|
||||
.references(() => tenants.id, { onDelete: 'cascade' }),
|
||||
accountId: text('account_id').notNull(),
|
||||
providerId: text('provider_id').notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => tenantAuthUsers.id, { onDelete: 'cascade' }),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
idToken: text('id_token'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at', { mode: 'string' }),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { mode: 'string' }),
|
||||
scope: text('scope'),
|
||||
password: text('password'),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
})
|
||||
|
||||
export const settings = pgTable(
|
||||
'settings',
|
||||
{
|
||||
@@ -402,9 +341,7 @@ export const dbSchema = {
|
||||
authAccounts,
|
||||
authVerifications,
|
||||
creemSubscriptions,
|
||||
tenantAuthUsers,
|
||||
tenantAuthSessions,
|
||||
tenantAuthAccounts,
|
||||
|
||||
settings,
|
||||
systemSettings,
|
||||
reactions,
|
||||
|
||||
Reference in New Issue
Block a user