mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-30 01:44:23 +00:00
update
This commit is contained in:
52
web/src/api/license/api.ts
Normal file
52
web/src/api/license/api.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import axiosInstance from '@/api/axiosInstance'
|
||||
|
||||
export interface LicenseStatusResponse {
|
||||
status: string
|
||||
email?: string | null
|
||||
edition?: string | null
|
||||
updates_until?: string | null
|
||||
features?: string[] | null
|
||||
days_remaining?: number | null
|
||||
build_date?: string | null
|
||||
valid_until?: string | null
|
||||
machine_id: string
|
||||
account_limit?: number | null
|
||||
accounts_used: number
|
||||
}
|
||||
|
||||
export interface UploadLicenseResponse {
|
||||
success: boolean
|
||||
email: string
|
||||
edition: string
|
||||
updates_until: string
|
||||
}
|
||||
|
||||
export async function get_license_status(): Promise<LicenseStatusResponse> {
|
||||
const { data } = await axiosInstance.get<LicenseStatusResponse>('api/v1/license/status')
|
||||
return data
|
||||
}
|
||||
|
||||
export async function upload_license(license: string): Promise<UploadLicenseResponse> {
|
||||
const { data } = await axiosInstance.post<UploadLicenseResponse>('api/v1/license/upload', {
|
||||
license,
|
||||
})
|
||||
return data
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
IconLayoutDashboard,
|
||||
IconSettings
|
||||
} from '@tabler/icons-react'
|
||||
import { IdCard, Inbox, Paperclip, Search, Upload, Users2, ScrollText } from 'lucide-react'
|
||||
import { BadgeCheck, IdCard, Inbox, Paperclip, Search, Upload, Users2, ScrollText } from 'lucide-react'
|
||||
import { type SidebarData } from '../types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||
@@ -34,6 +34,7 @@ export function useSidebarData(): SidebarData {
|
||||
const { require_any_permission } = useCurrentUser()
|
||||
const { features } = useEdition()
|
||||
const auditEnabled = features.includes('audit_log')
|
||||
const licenseEnabled = features.includes('license')
|
||||
|
||||
return {
|
||||
navGroups: [
|
||||
@@ -107,6 +108,12 @@ export function useSidebarData(): SidebarData {
|
||||
url: '/api-docs',
|
||||
icon: IconHelp,
|
||||
},
|
||||
{
|
||||
title: t('navigation.license'),
|
||||
url: '/license',
|
||||
icon: BadgeCheck,
|
||||
visible: licenseEnabled && require_any_permission(['system:root', 'user:manage']),
|
||||
},
|
||||
{
|
||||
title: t('navigation.auditLog'),
|
||||
url: '/audit-log',
|
||||
|
||||
290
web/src/features/license/index.tsx
Normal file
290
web/src/features/license/index.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { AxiosError } from 'axios'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Copy, FileUp, Loader2 } from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { FixedHeader } from '@/components/layout/fixed-header'
|
||||
import { Main } from '@/components/layout/main'
|
||||
import { useEdition } from '@/hooks/use-edition'
|
||||
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
import {
|
||||
get_license_status,
|
||||
upload_license,
|
||||
type LicenseStatusResponse,
|
||||
} from '@/api/license/api'
|
||||
|
||||
function formatEpoch(ts?: string | null): string {
|
||||
if (!ts) return '—'
|
||||
const n = Number(ts)
|
||||
if (!Number.isFinite(n)) return ts
|
||||
return new Date(n * 1000).toLocaleDateString()
|
||||
}
|
||||
|
||||
function InfoRow({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className='flex items-start justify-between gap-4 py-2'>
|
||||
<span className='text-sm text-muted-foreground'>{label}</span>
|
||||
<div className='flex flex-wrap items-center justify-end gap-1.5 text-right text-sm'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function LicensePage() {
|
||||
const { t } = useTranslation()
|
||||
const { toast } = useToast()
|
||||
const queryClient = useQueryClient()
|
||||
const { isPro, features } = useEdition()
|
||||
const { require_any_permission } = useCurrentUser()
|
||||
const [licenseText, setLicenseText] = useState('')
|
||||
|
||||
const { data, isLoading, error } = useQuery<LicenseStatusResponse, AxiosError>({
|
||||
queryKey: ['license-status'],
|
||||
queryFn: get_license_status,
|
||||
retry: false,
|
||||
})
|
||||
|
||||
const upload = useMutation({
|
||||
mutationFn: upload_license,
|
||||
onSuccess: () => {
|
||||
toast({ title: t('license.uploadSuccess') })
|
||||
setLicenseText('')
|
||||
queryClient.invalidateQueries({ queryKey: ['license-status'] })
|
||||
},
|
||||
onError: (err: AxiosError<{ error?: string }>) => {
|
||||
toast({
|
||||
title: t('license.uploadFailed'),
|
||||
description: err.response?.data?.error ?? t('license.uploadFailedDesc'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const copyMachineId = async () => {
|
||||
if (!data?.machine_id) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(data.machine_id)
|
||||
toast({ title: t('license.copied') })
|
||||
} catch {
|
||||
toast({ title: t('license.copyFailed'), variant: 'destructive' })
|
||||
}
|
||||
}
|
||||
|
||||
const onFilePicked = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => setLicenseText(String(reader.result ?? '').trim())
|
||||
reader.onerror = () => toast({ title: t('license.readFileFailed'), variant: 'destructive' })
|
||||
reader.readAsText(file)
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
const licenseEnabled = features.includes('license')
|
||||
const canView = isPro && licenseEnabled && require_any_permission(['system:root', 'user:manage'])
|
||||
|
||||
if (!canView) {
|
||||
return (
|
||||
<>
|
||||
<FixedHeader />
|
||||
<Main>
|
||||
<div className='mx-auto w-full max-w-7xl px-4 py-16 text-center text-muted-foreground'>
|
||||
{t('license.forbidden', 'License management is available in the Pro edition only.')}
|
||||
</div>
|
||||
</Main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
valid: t('license.statusValid'),
|
||||
trial: t('license.statusTrial'),
|
||||
trial_expired: t('license.statusTrialExpired'),
|
||||
update_expired: t('license.statusUpdateExpired'),
|
||||
machine_mismatch: t('license.statusMachineMismatch'),
|
||||
invalid_signature: t('license.statusInvalid'),
|
||||
error: t('license.statusError'),
|
||||
}
|
||||
|
||||
const statusVariant: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
||||
valid: 'default',
|
||||
trial: 'secondary',
|
||||
}
|
||||
|
||||
const status = data?.status ?? ''
|
||||
|
||||
return (
|
||||
<>
|
||||
<FixedHeader />
|
||||
<Main>
|
||||
<div className='mx-auto w-full max-w-7xl px-4'>
|
||||
<h1 className='mb-1 text-xl font-semibold'>{t('license.title')}</h1>
|
||||
<p className='mb-6 text-sm text-muted-foreground'>{t('license.description')}</p>
|
||||
|
||||
{isLoading && (
|
||||
<div className='flex h-40 items-center justify-center'>
|
||||
<Loader2 className='h-6 w-6 animate-spin' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !isLoading && (
|
||||
<div className='mb-6 rounded-md border border-destructive/50 p-4 text-sm text-destructive'>
|
||||
{t('license.loadFailed')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data && (
|
||||
<div className='grid gap-6 lg:grid-cols-2'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('license.statusTitle')}</CardTitle>
|
||||
<CardDescription>{t('license.statusDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='divide-y'>
|
||||
<InfoRow label={t('license.status')}>
|
||||
<Badge variant={statusVariant[status] ?? 'destructive'}>
|
||||
{statusLabels[status] ?? status}
|
||||
</Badge>
|
||||
</InfoRow>
|
||||
<InfoRow label={t('license.edition')}>
|
||||
{data.edition ? (
|
||||
<Badge variant='outline'>{data.edition}</Badge>
|
||||
) : (
|
||||
t('license.notAvailable')
|
||||
)}
|
||||
</InfoRow>
|
||||
<InfoRow label={t('license.licensee')}>
|
||||
{data.email ?? t('license.notAvailable')}
|
||||
</InfoRow>
|
||||
<InfoRow label={t('license.updatesUntil')}>
|
||||
{formatEpoch(data.updates_until)}
|
||||
</InfoRow>
|
||||
{data.days_remaining !== null && data.days_remaining !== undefined && (
|
||||
<InfoRow label={t('license.trialDays')}>
|
||||
{t('license.trialDaysRemaining', { days: data.days_remaining })}
|
||||
</InfoRow>
|
||||
)}
|
||||
<InfoRow label={t('license.accounts')}>
|
||||
{data.account_limit
|
||||
? t('license.accountsUsed', {
|
||||
used: data.accounts_used,
|
||||
limit: data.account_limit,
|
||||
})
|
||||
: t('license.notAvailable')}
|
||||
</InfoRow>
|
||||
<InfoRow label={t('license.features')}>
|
||||
{data.features && data.features.length > 0 ? (
|
||||
data.features.map((f) => (
|
||||
<Badge key={f} variant='outline' className='normal-case'>
|
||||
{f}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
t('license.notAvailable')
|
||||
)}
|
||||
</InfoRow>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className='flex flex-col gap-6'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('license.machineIdTitle')}</CardTitle>
|
||||
<CardDescription>{t('license.machineIdDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='flex items-center gap-2'>
|
||||
<code className='min-w-0 flex-1 break-all rounded-md bg-muted px-3 py-2 font-mono text-xs'>
|
||||
{data.machine_id || t('license.notAvailable')}
|
||||
</code>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon'
|
||||
onClick={copyMachineId}
|
||||
title={t('license.copyMachineId')}
|
||||
disabled={!data.machine_id}
|
||||
>
|
||||
<Copy className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('license.uploadTitle')}</CardTitle>
|
||||
<CardDescription>{t('license.uploadDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='flex flex-col gap-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Label
|
||||
htmlFor='license-file'
|
||||
className='inline-flex h-9 cursor-pointer items-center gap-2 rounded-md border border-input bg-background px-3 text-sm font-medium shadow-sm transition-colors hover:bg-accent'
|
||||
>
|
||||
<FileUp className='h-4 w-4' />
|
||||
{t('license.chooseFile')}
|
||||
<input
|
||||
id='license-file'
|
||||
type='file'
|
||||
accept='.jwt,.txt,text/plain,application/json'
|
||||
className='hidden'
|
||||
onChange={onFilePicked}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
<Textarea
|
||||
value={licenseText}
|
||||
onChange={(e) => setLicenseText(e.target.value)}
|
||||
placeholder={t('license.pasteHere')}
|
||||
rows={5}
|
||||
className='font-mono text-xs'
|
||||
/>
|
||||
<Button
|
||||
onClick={() => upload.mutate(licenseText.trim())}
|
||||
disabled={!licenseText.trim() || upload.isPending}
|
||||
className='self-start'
|
||||
>
|
||||
{upload.isPending && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||
{upload.isPending ? t('license.uploading') : t('license.upload')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -781,12 +781,52 @@
|
||||
"successCount": "{{count}} imported",
|
||||
"target": "1. Select target account",
|
||||
"title": "Import",
|
||||
"uploading": "Uploading…",
|
||||
"uploadingFile": "Uploading file",
|
||||
"willImportTo": "Will import to"
|
||||
"uploading": "Uploading…",
|
||||
"uploadingFile": "Uploading file",
|
||||
"willImportTo": "Will import to"
|
||||
},
|
||||
"license": {
|
||||
"title": "License",
|
||||
"description": "Manage your machine-bound license: view the machine ID, upload a license, and check its status.",
|
||||
"statusTitle": "License status",
|
||||
"statusDesc": "Current status of the license on this server.",
|
||||
"status": "Status",
|
||||
"statusValid": "Valid",
|
||||
"statusTrial": "Trial",
|
||||
"statusTrialExpired": "Trial expired",
|
||||
"statusUpdateExpired": "Update coverage expired",
|
||||
"statusMachineMismatch": "Machine mismatch",
|
||||
"statusInvalid": "Invalid",
|
||||
"statusError": "Error",
|
||||
"edition": "Edition",
|
||||
"licensee": "Licensee",
|
||||
"updatesUntil": "Updates until",
|
||||
"features": "Enabled features",
|
||||
"accounts": "IMAP accounts",
|
||||
"accountsUsed": "{{used}} / {{limit}}",
|
||||
"trialDays": "Trial",
|
||||
"trialDaysRemaining": "{{days}} days remaining",
|
||||
"machineIdTitle": "Machine ID",
|
||||
"machineIdDesc": "Send this ID to your vendor to obtain a license bound to this machine.",
|
||||
"copyMachineId": "Copy machine ID",
|
||||
"copied": "Machine ID copied to clipboard",
|
||||
"copyFailed": "Failed to copy machine ID",
|
||||
"uploadTitle": "Upload license",
|
||||
"uploadDesc": "Paste the license JWT from your vendor, or select the license file.",
|
||||
"pasteHere": "Paste the license JWT here...",
|
||||
"chooseFile": "Choose file",
|
||||
"readFileFailed": "Failed to read the selected file",
|
||||
"upload": "Upload",
|
||||
"uploading": "Uploading...",
|
||||
"uploadSuccess": "License uploaded and activated",
|
||||
"uploadFailed": "License upload failed",
|
||||
"uploadFailedDesc": "The license could not be uploaded. Check the license and try again.",
|
||||
"loadFailed": "Failed to load license status.",
|
||||
"notAvailable": "—",
|
||||
"forbidden": "License management is available in the Pro edition only."
|
||||
},
|
||||
"mail": {
|
||||
"account": "Account",
|
||||
"account": "Account",
|
||||
"attachments": "Attachments",
|
||||
"bcc": "BCC",
|
||||
"blockRemoteAgain": "Block again",
|
||||
@@ -879,6 +919,7 @@
|
||||
"dashboard": "Dashboard",
|
||||
"general": "General",
|
||||
"home": "Home",
|
||||
"license": "License",
|
||||
"mailbox": "Mailbox",
|
||||
"oauth2": "OAuth2",
|
||||
"other": "Other",
|
||||
@@ -1914,4 +1955,4 @@
|
||||
"singleRequestBatchSizeTooLarge": "Batch size must be at most 200",
|
||||
"singleRequestBatchSizeTooSmall": "Batch size must be at least 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,6 +783,46 @@
|
||||
"uploadingFile": "正在上传文件",
|
||||
"willImportTo": "将导入至"
|
||||
},
|
||||
"license": {
|
||||
"title": "许可证",
|
||||
"description": "管理绑定到本机的许可证:查看机器码、上传许可证并检查状态。",
|
||||
"statusTitle": "许可证状态",
|
||||
"statusDesc": "当前服务器许可证的状态。",
|
||||
"status": "状态",
|
||||
"statusValid": "有效",
|
||||
"statusTrial": "试用中",
|
||||
"statusTrialExpired": "试用已过期",
|
||||
"statusUpdateExpired": "更新服务已过期",
|
||||
"statusMachineMismatch": "机器码不匹配",
|
||||
"statusInvalid": "无效",
|
||||
"statusError": "错误",
|
||||
"edition": "版本",
|
||||
"licensee": "授权用户",
|
||||
"updatesUntil": "更新截止",
|
||||
"features": "已启用功能",
|
||||
"accounts": "IMAP 账户",
|
||||
"accountsUsed": "{{used}} / {{limit}}",
|
||||
"trialDays": "试用",
|
||||
"trialDaysRemaining": "剩余 {{days}} 天",
|
||||
"machineIdTitle": "机器码",
|
||||
"machineIdDesc": "将此机器码发送给厂商,以获得绑定到本机的许可证。",
|
||||
"copyMachineId": "复制机器码",
|
||||
"copied": "机器码已复制到剪贴板",
|
||||
"copyFailed": "复制机器码失败",
|
||||
"uploadTitle": "上传许可证",
|
||||
"uploadDesc": "粘贴厂商提供的许可证 JWT,或选择许可证文件。",
|
||||
"pasteHere": "在此粘贴许可证 JWT...",
|
||||
"chooseFile": "选择文件",
|
||||
"readFileFailed": "读取所选文件失败",
|
||||
"upload": "上传",
|
||||
"uploading": "上传中...",
|
||||
"uploadSuccess": "许可证已上传并生效",
|
||||
"uploadFailed": "许可证上传失败",
|
||||
"uploadFailedDesc": "许可证无法上传,请检查许可证后重试。",
|
||||
"loadFailed": "加载许可证状态失败。",
|
||||
"notAvailable": "—",
|
||||
"forbidden": "许可证管理仅在 Pro 版本中可用。"
|
||||
},
|
||||
"mail": {
|
||||
"account": "账户",
|
||||
"attachments": "附件",
|
||||
@@ -874,9 +914,10 @@
|
||||
"attachment": "附件",
|
||||
"auth": "认证",
|
||||
"dashboard": "仪表板",
|
||||
"general": "常规",
|
||||
"home": "首页",
|
||||
"mailbox": "邮箱",
|
||||
"general": "常规",
|
||||
"home": "首页",
|
||||
"license": "许可证",
|
||||
"mailbox": "邮箱",
|
||||
"oauth2": "OAuth2",
|
||||
"other": "其他",
|
||||
"settings": "设置",
|
||||
@@ -1907,4 +1948,4 @@
|
||||
"singleRequestBatchSizeTooLarge": "批大小必须最多为200",
|
||||
"singleRequestBatchSizeTooSmall": "批大小必须至少为10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ import { Route as AuthenticatedAttachmentIndexImport } from './routes/_authentic
|
||||
|
||||
// Create Virtual Routes
|
||||
|
||||
const AuthenticatedLicenseLazyImport = createFileRoute(
|
||||
'/_authenticated/license',
|
||||
)()
|
||||
const AuthenticatedAuditLogLazyImport = createFileRoute(
|
||||
'/_authenticated/audit-log',
|
||||
)()
|
||||
@@ -99,6 +102,14 @@ const AuthenticatedIndexRoute = AuthenticatedIndexImport.update({
|
||||
getParentRoute: () => AuthenticatedRouteRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticatedLicenseLazyRoute = AuthenticatedLicenseLazyImport.update({
|
||||
id: '/license',
|
||||
path: '/license',
|
||||
getParentRoute: () => AuthenticatedRouteRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/_authenticated/license.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const AuthenticatedAuditLogLazyRoute = AuthenticatedAuditLogLazyImport.update({
|
||||
id: '/audit-log',
|
||||
path: '/audit-log',
|
||||
@@ -435,6 +446,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedAuditLogLazyImport
|
||||
parentRoute: typeof AuthenticatedRouteImport
|
||||
}
|
||||
'/_authenticated/license': {
|
||||
id: '/_authenticated/license'
|
||||
path: '/license'
|
||||
fullPath: '/license'
|
||||
preLoaderRoute: typeof AuthenticatedLicenseLazyImport
|
||||
parentRoute: typeof AuthenticatedRouteImport
|
||||
}
|
||||
'/_authenticated/': {
|
||||
id: '/_authenticated/'
|
||||
path: '/'
|
||||
@@ -632,6 +650,7 @@ interface AuthenticatedRouteRouteChildren {
|
||||
AuthenticatedSettingsRouteLazyRoute: typeof AuthenticatedSettingsRouteLazyRouteWithChildren
|
||||
AuthenticatedUsersRouteLazyRoute: typeof AuthenticatedUsersRouteLazyRouteWithChildren
|
||||
AuthenticatedAuditLogLazyRoute: typeof AuthenticatedAuditLogLazyRoute
|
||||
AuthenticatedLicenseLazyRoute: typeof AuthenticatedLicenseLazyRoute
|
||||
AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
|
||||
AuthenticatedAccountsNewLazyRoute: typeof AuthenticatedAccountsNewLazyRoute
|
||||
AuthenticatedAttachmentIndexRoute: typeof AuthenticatedAttachmentIndexRoute
|
||||
@@ -650,6 +669,7 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
|
||||
AuthenticatedUsersRouteLazyRoute:
|
||||
AuthenticatedUsersRouteLazyRouteWithChildren,
|
||||
AuthenticatedAuditLogLazyRoute: AuthenticatedAuditLogLazyRoute,
|
||||
AuthenticatedLicenseLazyRoute: AuthenticatedLicenseLazyRoute,
|
||||
AuthenticatedIndexRoute: AuthenticatedIndexRoute,
|
||||
AuthenticatedAccountsNewLazyRoute: AuthenticatedAccountsNewLazyRoute,
|
||||
AuthenticatedAttachmentIndexRoute: AuthenticatedAttachmentIndexRoute,
|
||||
@@ -678,6 +698,7 @@ export interface FileRoutesByFullPath {
|
||||
'/404': typeof errors404LazyRoute
|
||||
'/503': typeof errors503LazyRoute
|
||||
'/audit-log': typeof AuthenticatedAuditLogLazyRoute
|
||||
'/license': typeof AuthenticatedLicenseLazyRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/accounts/new': typeof AuthenticatedAccountsNewLazyRoute
|
||||
'/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
|
||||
@@ -708,6 +729,7 @@ export interface FileRoutesByTo {
|
||||
'/404': typeof errors404LazyRoute
|
||||
'/503': typeof errors503LazyRoute
|
||||
'/audit-log': typeof AuthenticatedAuditLogLazyRoute
|
||||
'/license': typeof AuthenticatedLicenseLazyRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/accounts/new': typeof AuthenticatedAccountsNewLazyRoute
|
||||
'/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
|
||||
@@ -743,6 +765,7 @@ export interface FileRoutesById {
|
||||
'/(errors)/500': typeof errors500LazyRoute
|
||||
'/(errors)/503': typeof errors503LazyRoute
|
||||
'/_authenticated/audit-log': typeof AuthenticatedAuditLogLazyRoute
|
||||
'/_authenticated/license': typeof AuthenticatedLicenseLazyRoute
|
||||
'/_authenticated/': typeof AuthenticatedIndexRoute
|
||||
'/_authenticated/accounts/new': typeof AuthenticatedAccountsNewLazyRoute
|
||||
'/_authenticated/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
|
||||
@@ -778,6 +801,7 @@ export interface FileRouteTypes {
|
||||
| '/404'
|
||||
| '/503'
|
||||
| '/audit-log'
|
||||
| '/license'
|
||||
| '/'
|
||||
| '/accounts/new'
|
||||
| '/settings/access'
|
||||
@@ -807,6 +831,7 @@ export interface FileRouteTypes {
|
||||
| '/404'
|
||||
| '/503'
|
||||
| '/audit-log'
|
||||
| '/license'
|
||||
| '/'
|
||||
| '/accounts/new'
|
||||
| '/settings/access'
|
||||
@@ -840,6 +865,7 @@ export interface FileRouteTypes {
|
||||
| '/(errors)/500'
|
||||
| '/(errors)/503'
|
||||
| '/_authenticated/audit-log'
|
||||
| '/_authenticated/license'
|
||||
| '/_authenticated/'
|
||||
| '/_authenticated/accounts/new'
|
||||
| '/_authenticated/settings/access'
|
||||
@@ -911,6 +937,7 @@ export const routeTree = rootRoute
|
||||
"/_authenticated/settings",
|
||||
"/_authenticated/users",
|
||||
"/_authenticated/audit-log",
|
||||
"/_authenticated/license",
|
||||
"/_authenticated/",
|
||||
"/_authenticated/accounts/new",
|
||||
"/_authenticated/attachment/",
|
||||
@@ -970,6 +997,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "_authenticated/audit-log.lazy.tsx",
|
||||
"parent": "/_authenticated"
|
||||
},
|
||||
"/_authenticated/license": {
|
||||
"filePath": "_authenticated/license.lazy.tsx",
|
||||
"parent": "/_authenticated"
|
||||
},
|
||||
"/_authenticated/": {
|
||||
"filePath": "_authenticated/index.tsx",
|
||||
"parent": "/_authenticated"
|
||||
|
||||
24
web/src/routes/_authenticated/license.lazy.tsx
Normal file
24
web/src/routes/_authenticated/license.lazy.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import LicensePage from '@/features/license'
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createLazyFileRoute('/_authenticated/license')({
|
||||
component: LicensePage,
|
||||
})
|
||||
Reference in New Issue
Block a user