Skip to content

Commit c300b5e

Browse files
committed
feat(server): onboarding and login layout design
- Replaced the existing GNU AGPL v3 license with the new Attribution Network License (ANL) v1.0 in the LICENSE file. - Updated package.json files to reflect the new license. - Modified README.md to reference the new license. - Adjusted various package.json files to remove outdated license entries. - Added new dependencies and updated existing ones in pnpm-lock.yaml. - Refactored onboarding components and improved UI consistency across the dashboard. - Introduced new hooks and components for better onboarding experience. Signed-off-by: Innei <tukon479@gmail.com>
1 parent 68ab78b commit c300b5e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1807
-571
lines changed

LICENSE

Lines changed: 316 additions & 166 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export async function customImageProcessor(buffer: Buffer) {
201201

202202
## 📄 License
203203

204-
AGPL v3 License © 2025 Afilmory Team
204+
Attribution Network License (ANL) v1.0 © 2025 Afilmory Team. See [LICENSE](LICENSE) for more details.
205205

206206
## 🔗 Related Links
207207

apps/ssr/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"packageManager": "pnpm@10.18.1",
66
"description": "",
77
"author": "Innei",
8-
"license": "ISC",
98
"main": "index.js",
109
"scripts": {
1110
"build": "sh scripts/build.sh",

apps/web/vite.config.ts

Lines changed: 75 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { defineConfig } from 'vite'
1212
import { analyzer } from 'vite-bundle-analyzer'
1313
import { checker } from 'vite-plugin-checker'
1414
import { createHtmlPlugin } from 'vite-plugin-html'
15+
import { VitePWA } from 'vite-plugin-pwa'
1516
import tsconfigPaths from 'vite-tsconfig-paths'
1617

1718
import PKG from '../../package.json'
@@ -81,80 +82,80 @@ export default defineConfig({
8182
manifestInjectPlugin(),
8283
photosStaticPlugin(),
8384
tailwindcss(),
84-
// VitePWA({
85-
// registerType: 'autoUpdate',
86-
// includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
87-
// manifest: {
88-
// name: siteConfig.title,
89-
// short_name: siteConfig.name,
90-
// description: siteConfig.description,
91-
// theme_color: '#1c1c1e',
92-
// background_color: '#1c1c1e',
93-
// display: 'standalone',
94-
// scope: '/',
95-
// start_url: '/',
96-
// icons: [
97-
// {
98-
// src: 'android-chrome-192x192.png',
99-
// sizes: '192x192',
100-
// type: 'image/png',
101-
// },
102-
// {
103-
// src: 'android-chrome-512x512.png',
104-
// sizes: '512x512',
105-
// type: 'image/png',
106-
// },
107-
// {
108-
// src: 'apple-touch-icon.png',
109-
// sizes: '180x180',
110-
// type: 'image/png',
111-
// },
112-
// ],
113-
// },
114-
// workbox: {
115-
// maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10MB
116-
// globPatterns: ['**/*.{js,css,html,ico,png,svg,webp}'],
117-
// globIgnores: ['**/*.{jpg,jpeg}'], // 忽略大图片文件
118-
// runtimeCaching: [
119-
// {
120-
// urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
121-
// handler: 'CacheFirst',
122-
// options: {
123-
// cacheName: 'google-fonts-cache',
124-
// expiration: {
125-
// maxEntries: 10,
126-
// maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
127-
// },
128-
// },
129-
// },
130-
// {
131-
// urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
132-
// handler: 'CacheFirst',
133-
// options: {
134-
// cacheName: 'gstatic-fonts-cache',
135-
// expiration: {
136-
// maxEntries: 10,
137-
// maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
138-
// },
139-
// },
140-
// },
141-
// {
142-
// urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/,
143-
// handler: 'CacheFirst',
144-
// options: {
145-
// cacheName: 'images-cache',
146-
// expiration: {
147-
// maxEntries: 100,
148-
// maxAgeSeconds: 60 * 60 * 24 * 30, // <== 30 days
149-
// },
150-
// },
151-
// },
152-
// ],
153-
// },
154-
// devOptions: {
155-
// enabled: false, // 开发环境不启用 PWA
156-
// },
157-
// }),
85+
VitePWA({
86+
registerType: 'autoUpdate',
87+
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
88+
manifest: {
89+
name: siteConfig.title,
90+
short_name: siteConfig.name,
91+
description: siteConfig.description,
92+
theme_color: '#1c1c1e',
93+
background_color: '#1c1c1e',
94+
display: 'standalone',
95+
scope: '/',
96+
start_url: '/',
97+
icons: [
98+
{
99+
src: 'android-chrome-192x192.png',
100+
sizes: '192x192',
101+
type: 'image/png',
102+
},
103+
{
104+
src: 'android-chrome-512x512.png',
105+
sizes: '512x512',
106+
type: 'image/png',
107+
},
108+
{
109+
src: 'apple-touch-icon.png',
110+
sizes: '180x180',
111+
type: 'image/png',
112+
},
113+
],
114+
},
115+
workbox: {
116+
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10MB
117+
globPatterns: ['**/*.{js,css,html,ico,png,svg,webp}'],
118+
globIgnores: ['**/*.{jpg,jpeg}'], // 忽略大图片文件
119+
runtimeCaching: [
120+
{
121+
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
122+
handler: 'CacheFirst',
123+
options: {
124+
cacheName: 'google-fonts-cache',
125+
expiration: {
126+
maxEntries: 10,
127+
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
128+
},
129+
},
130+
},
131+
{
132+
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
133+
handler: 'CacheFirst',
134+
options: {
135+
cacheName: 'gstatic-fonts-cache',
136+
expiration: {
137+
maxEntries: 10,
138+
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
139+
},
140+
},
141+
},
142+
{
143+
urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/,
144+
handler: 'CacheFirst',
145+
options: {
146+
cacheName: 'images-cache',
147+
expiration: {
148+
maxEntries: 100,
149+
maxAgeSeconds: 60 * 60 * 24 * 30, // <== 30 days
150+
},
151+
},
152+
},
153+
],
154+
},
155+
devOptions: {
156+
enabled: false, // 开发环境不启用 PWA
157+
},
158+
}),
158159
ogImagePlugin({
159160
title: siteConfig.title,
160161
description: siteConfig.description,

be/apps/core/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"version": "1.0.0",
55
"packageManager": "pnpm@10.18.0",
66
"author": "Innei",
7-
"license": "MIT",
87
"main": "index.ts",
98
"scripts": {
109
"build": "vite build",

be/apps/core/src/database/database.provider.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { AsyncLocalStorage } from 'node:async_hooks'
22

33
import { dbSchema } from '@afilmory/db'
44
import { createLogger } from '@afilmory/framework'
5+
import { BizException, ErrorCode } from 'core/errors'
6+
import { getTenantContext } from 'core/modules/tenant/tenant.context'
57
import { drizzle } from 'drizzle-orm/node-postgres'
68
import { Pool } from 'pg'
79
import { injectable } from 'tsyringe'
810

9-
import { BizException, ErrorCode } from 'core/errors'
10-
import { getTenantContext } from 'core/modules/tenant/tenant.context'
1111
import { DatabaseConfig } from './database.config'
1212
import type { DatabaseContextStore, DrizzleDb } from './tokens'
1313

@@ -55,14 +55,14 @@ export async function applyTenantIsolationContext(options?: {
5555
return
5656
}
5757

58-
const client = store.transaction.client
58+
const { client } = store.transaction
5959

60-
await client.query('SET LOCAL afilmory.is_superadmin = $1', [isSuperAdmin ? 'true' : 'false'])
60+
await client.query("SELECT set_config('afilmory.is_superadmin', $1, true)", [isSuperAdmin ? 'true' : 'false'])
6161

6262
if (isSuperAdmin) {
6363
await client.query('RESET afilmory.tenant_id')
6464
} else if (tenantId) {
65-
await client.query('SET LOCAL afilmory.tenant_id = $1', [tenantId])
65+
await client.query("SELECT set_config('afilmory.tenant_id', $1, true)", [tenantId])
6666
}
6767

6868
store.tenantIsolation = {

be/apps/core/src/middlewares/cors.middleware.ts

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { HttpMiddleware, OnModuleDestroy, OnModuleInit } from '@afilmory/framework'
22
import { EventEmitterService, Middleware } from '@afilmory/framework'
3+
import { OnboardingService } from 'core/modules/onboarding/onboarding.service'
34
import type { Context, Next } from 'hono'
45
import { cors } from 'hono/cors'
56
import { injectable } from 'tsyringe'
@@ -42,26 +43,25 @@ function parseAllowedOrigins(raw: string | null): AllowedOrigins {
4243
return Array.from(new Set(entries))
4344
}
4445

45-
@Middleware({ path: '/*', priority: -100 })
46+
@Middleware()
4647
@injectable()
4748
export class CorsMiddleware implements HttpMiddleware, OnModuleInit, OnModuleDestroy {
4849
private readonly allowedOrigins = new Map<string, AllowedOrigins>()
4950
private defaultTenantId?: string
5051
private readonly logger = logger.extend('CorsMiddleware')
52+
constructor(
53+
private readonly eventEmitter: EventEmitterService,
54+
private readonly settingService: SettingService,
55+
private readonly tenantService: TenantService,
56+
private readonly onboardingService: OnboardingService,
57+
) {}
58+
5159
private readonly corsMiddleware = cors({
5260
origin: (origin) => this.resolveOrigin(origin),
5361
credentials: true,
5462
})
5563

56-
private readonly handleSettingUpdated = ({
57-
tenantId,
58-
key,
59-
value,
60-
}: {
61-
tenantId: string
62-
key: string
63-
value: string
64-
}) => {
64+
private readonly handleSettingUpdated = ({ tenantId, key }: { tenantId: string; key: string }) => {
6565
if (key !== 'http.cors.allowedOrigins') {
6666
return
6767
}
@@ -75,12 +75,6 @@ export class CorsMiddleware implements HttpMiddleware, OnModuleInit, OnModuleDes
7575
this.allowedOrigins.delete(tenantId)
7676
}
7777

78-
constructor(
79-
private readonly eventEmitter: EventEmitterService,
80-
private readonly settingService: SettingService,
81-
private readonly tenantService: TenantService,
82-
) {}
83-
8478
async onModuleInit(): Promise<void> {
8579
try {
8680
const defaultTenant = await this.tenantService.getDefaultTenant()
@@ -98,14 +92,61 @@ export class CorsMiddleware implements HttpMiddleware, OnModuleInit, OnModuleDes
9892
this.eventEmitter.off('setting.deleted', this.handleSettingDeleted)
9993
}
10094

95+
private addAllCorsHeaders(context: Context): void {
96+
context.res.headers.set('Access-Control-Allow-Origin', context.req.header('Origin') ?? '*')
97+
context.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
98+
context.res.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
99+
context.res.headers.set('Access-Control-Allow-Credentials', 'true')
100+
}
101+
102+
private handleOptionsPreflight(context: Context): Response {
103+
const origin = context.req.header('Origin')
104+
105+
if (origin) {
106+
context.res.headers.append('Vary', 'Origin')
107+
}
108+
109+
// Align with typical CORS preflight behavior
110+
const requestHeaders = context.req.header('Access-Control-Request-Headers')
111+
if (requestHeaders) {
112+
context.res.headers.set('Access-Control-Allow-Headers', requestHeaders)
113+
context.res.headers.append('Vary', 'Access-Control-Request-Headers')
114+
}
115+
116+
context.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
117+
context.res.headers.set('Access-Control-Allow-Credentials', 'true')
118+
context.res.headers.set('Access-Control-Allow-Origin', origin ?? '*')
119+
120+
// Ensure no body-related headers on 204
121+
context.res.headers.delete('Content-Length')
122+
context.res.headers.delete('Content-Type')
123+
124+
return new Response(null, {
125+
headers: context.res.headers,
126+
status: 204,
127+
statusText: 'No Content',
128+
})
129+
}
130+
101131
async use(context: Context, next: Next): Promise<Response | void> {
132+
const initialized = await this.onboardingService.isInitialized()
133+
134+
if (!initialized) {
135+
this.logger.info(`Application not initialized yet, skip CORS middleware for ${context.req.path}`)
136+
if (context.req.method === 'OPTIONS') {
137+
return this.handleOptionsPreflight(context)
138+
}
139+
this.addAllCorsHeaders(context)
140+
return await next()
141+
}
142+
102143
const tenantContext = getTenantContext()
103144
const tenantId = tenantContext?.tenant.id ?? this.defaultTenantId
104145

105146
if (tenantId) {
106147
await this.ensureTenantOriginsLoaded(tenantId)
107148
} else {
108-
this.logger.warn('Tenant context missing for request %s %s', context.req.method, context.req.path)
149+
this.logger.warn(`Tenant context missing for request ${context.req.method} ${context.req.path}`)
109150
}
110151

111152
return await this.corsMiddleware(context, next)
@@ -125,7 +166,7 @@ export class CorsMiddleware implements HttpMiddleware, OnModuleInit, OnModuleDes
125166
try {
126167
raw = await this.settingService.get('http.cors.allowedOrigins', { tenantId })
127168
} catch (error) {
128-
this.logger.warn('Failed to load CORS configuration from settings for tenant %s', tenantId, error)
169+
this.logger.warn('Failed to load CORS configuration from settings for tenant', tenantId, error)
129170
}
130171

131172
this.updateAllowedOrigins(tenantId, raw)
@@ -134,11 +175,7 @@ export class CorsMiddleware implements HttpMiddleware, OnModuleInit, OnModuleDes
134175
private updateAllowedOrigins(tenantId: string, next: string | null): void {
135176
const parsed = parseAllowedOrigins(next)
136177
this.allowedOrigins.set(tenantId, parsed)
137-
this.logger.info(
138-
'Updated CORS allowed origins for tenant %s %s',
139-
tenantId,
140-
parsed === '*' ? '*' : JSON.stringify(parsed),
141-
)
178+
this.logger.info('Updated CORS allowed origins for tenant', tenantId, parsed === '*' ? '*' : JSON.stringify(parsed))
142179
}
143180

144181
private resolveOrigin(origin: string | undefined): string | null {

be/apps/core/src/middlewares/database-context.middleware.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import type { HttpMiddleware } from '@afilmory/framework'
22
import { Middleware } from '@afilmory/framework'
3+
import {
4+
applyTenantIsolationContext,
5+
getOptionalDbContext,
6+
PgPoolProvider,
7+
runWithDbContext,
8+
} from 'core/database/database.provider'
9+
import { getTenantContext } from 'core/modules/tenant/tenant.context'
310
import type { Context, Next } from 'hono'
411
import { injectable } from 'tsyringe'
512

6-
import { applyTenantIsolationContext, getOptionalDbContext, PgPoolProvider, runWithDbContext } from 'core/database/database.provider'
7-
import { getTenantContext } from 'core/modules/tenant/tenant.context'
813
import { logger } from '../helpers/logger.helper'
914

10-
@Middleware({ path: '/*', priority: -180 })
15+
@Middleware()
1116
@injectable()
1217
export class DatabaseContextMiddleware implements HttpMiddleware {
1318
private readonly log = logger.extend('DatabaseContext')

0 commit comments

Comments
 (0)