feat(auth): update AUTH_UI_URL to point to accounts.airi.build
This commit is contained in:
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
environment:
|
||||
name: Auth Production
|
||||
url: https://auth.airi.build/ui/
|
||||
url: https://accounts.airi.build/ui/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -32,7 +32,7 @@ docker compose -f apps/server/docker-compose.otel.yml up -d
|
||||
|
||||
Default:
|
||||
|
||||
`AUTH_UI_URL=https://auth.airi.build/ui`
|
||||
`AUTH_UI_URL=https://accounts.airi.build/ui`
|
||||
|
||||
Set this when previewing or deploying auth UI to a different Cloudflare URL.
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ Out:
|
||||
|
||||
- **Resend SDK**:使用官方 `resend` npm 包。错误处理走 `errorMessageFrom`(`@moeru/std`);失败时抛 `ApiError(502, 'email/send_failed', ...)` 让 Better Auth 把错传回前端。
|
||||
- **触发邮件的位置**:Better Auth 的 hook 是 server 内部回调,不是 HTTP 路由——跨实例时只有处理该次 sign-in/up 的实例会触发,不会重复。
|
||||
- **Verify / reset 链接 URL**:链接落地页不放 `apps/server`,而是放独立部署的 `apps/ui-server-auth`。`API_SERVER_URL` 是 server 自身(如 `https://api.airi.build`),ui-server-auth 是同站点的另一域(如 `https://auth.airi.build/ui`);两者通过 trustedOrigins 互信。链接组装规则:
|
||||
- **Verify / reset 链接 URL**:链接落地页不放 `apps/server`,而是放独立部署的 `apps/ui-server-auth`。`API_SERVER_URL` 是 server 自身(如 `https://api.airi.build`),ui-server-auth 是同站点的另一域(如 `https://accounts.airi.build/ui`);两者通过 trustedOrigins 互信。链接组装规则:
|
||||
- Verify email:`<UI_BASE>/verify-email?token=<token>`
|
||||
- Reset password:`<UI_BASE>/reset-password?token=<token>`
|
||||
- 由 `getAuthTrustedOrigins(request)` 第一个匹配的 origin 决定 `<UI_BASE>`,避免硬编码。
|
||||
|
||||
@@ -83,7 +83,7 @@ const EnvSchema = object({
|
||||
// Standalone auth UI base URL. The server keeps `/auth/*` as the historical
|
||||
// entrypoint and redirects those requests here after ui-server-auth moved out
|
||||
// of the server image.
|
||||
AUTH_UI_URL: optional(string(), 'https://auth.airi.build/ui'),
|
||||
AUTH_UI_URL: optional(string(), 'https://accounts.airi.build/ui'),
|
||||
|
||||
// Standalone admin UI base URL. The server keeps `/admin/*` as the historical
|
||||
// entrypoint and redirects those requests here after ui-admin moved out of
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { Database } from '../db'
|
||||
import type { Env } from '../env'
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createAuth, ensureDynamicFirstPartyRedirectUri, seedTrustedClients } from '../auth'
|
||||
@@ -31,7 +34,7 @@ function createMockDb(existingRowsByCall: unknown[][] = []) {
|
||||
|
||||
describe('createAuth', () => {
|
||||
it('allows signed-in users to link OAuth accounts that use a different email', () => {
|
||||
const auth = createAuth({} as any, {
|
||||
const auth = createAuth({} as unknown as Database, {
|
||||
API_SERVER_URL: 'http://localhost:3000',
|
||||
AUTH_GOOGLE_CLIENT_ID: 'google-client',
|
||||
AUTH_GOOGLE_CLIENT_SECRET: 'google-secret',
|
||||
@@ -39,7 +42,7 @@ describe('createAuth', () => {
|
||||
AUTH_GITHUB_CLIENT_SECRET: 'github-secret',
|
||||
BETTER_AUTH_SECRET: 'test-secret-test-secret-test-secret',
|
||||
ADDITIONAL_TRUSTED_ORIGINS: [],
|
||||
} as any)
|
||||
} as unknown as Env)
|
||||
|
||||
expect(auth.options.account?.accountLinking?.allowDifferentEmails).toBe(true)
|
||||
})
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('parseEnv', () => {
|
||||
|
||||
expect(env.DATABASE_URL).toBe('postgres://example')
|
||||
expect(env.REDIS_URL).toBe('redis://example')
|
||||
expect(env.AUTH_UI_URL).toBe('https://auth.airi.build/ui')
|
||||
expect(env.AUTH_UI_URL).toBe('https://accounts.airi.build/ui')
|
||||
expect(env.ADMIN_UI_URL).toBe('https://admin.airi.build')
|
||||
expect(env.ADDITIONAL_TRUSTED_ORIGINS).toEqual([])
|
||||
})
|
||||
|
||||
@@ -43,7 +43,7 @@ async function buildRoutes(currentUser: SessionUser) {
|
||||
db: {} as any, // userinfo path never queries the DB
|
||||
env: {
|
||||
API_SERVER_URL: 'http://localhost:3000',
|
||||
AUTH_UI_URL: 'https://auth.airi.build/ui',
|
||||
AUTH_UI_URL: 'https://accounts.airi.build/ui',
|
||||
ADDITIONAL_TRUSTED_ORIGINS: [],
|
||||
} as any,
|
||||
configKV: createConfigKV(),
|
||||
@@ -103,7 +103,7 @@ describe('auth UI routes', () => {
|
||||
expect(res.status).toBe(302)
|
||||
|
||||
const location = res.headers.get('location')
|
||||
expect(location).toBe('https://auth.airi.build/ui/sign-in?provider=github&client_id=stage-web&prompt=login&redirect_uri=http%3A%2F%2Flocalhost%3A5173%2Fauth%2Fcallback&api_server_url=http%3A%2F%2Flocalhost%3A3000')
|
||||
expect(location).toBe('https://accounts.airi.build/ui/sign-in?provider=github&client_id=stage-web&prompt=login&redirect_uri=http%3A%2F%2Flocalhost%3A5173%2Fauth%2Fcallback&api_server_url=http%3A%2F%2Flocalhost%3A3000')
|
||||
})
|
||||
|
||||
it('redirects Electron OIDC callback queries to the standalone auth UI relay', async () => {
|
||||
@@ -112,6 +112,6 @@ describe('auth UI routes', () => {
|
||||
const res = await routes.request('/api/auth/oidc/electron-callback?code=sample-code&state=43123%3Aopaque-state')
|
||||
|
||||
expect(res.status).toBe(302)
|
||||
expect(res.headers.get('location')).toBe('https://auth.airi.build/ui/api/auth/oidc/electron-callback?code=sample-code&state=43123%3Aopaque-state')
|
||||
expect(res.headers.get('location')).toBe('https://accounts.airi.build/ui/api/auth/oidc/electron-callback?code=sample-code&state=43123%3Aopaque-state')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const SERVER_AUTH_UI_BASE_PATH = '/auth'
|
||||
export const AUTH_UI_API_SERVER_URL_QUERY_PARAM = 'api_server_url'
|
||||
export const DEFAULT_AUTH_UI_URL = 'https://auth.airi.build/ui'
|
||||
export const DEFAULT_AUTH_UI_URL = 'https://accounts.airi.build/ui'
|
||||
export const SERVER_DEV_API_SERVER_URL = 'https://airi-server-dev.up.railway.app'
|
||||
export const SERVER_DEV_AUTH_UI_URL = 'https://server-dev.airi-server-auth.pages.dev/ui'
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const TRUSTED_EXACT_ORIGINS = [
|
||||
'capacitor://localhost', // Capacitor mobile (iOS)
|
||||
'ai.moeru.airi-pocket://links', // Android deep link
|
||||
'https://airi.moeru.ai', // Production
|
||||
'https://auth.airi.build', // Standalone auth UI
|
||||
'https://accounts.airi.build', // Standalone auth UI
|
||||
'https://server-dev.airi-server-auth.pages.dev', // Server-dev standalone auth UI
|
||||
'https://admin.airi.build', // Standalone admin UI
|
||||
'https://server-dev.airi-server-admin.pages.dev', // Server-dev standalone admin UI
|
||||
|
||||
@@ -4,16 +4,16 @@ import { buildAuthUiRedirectUrl, buildAuthUiUrl, resolveAuthUiUrl } from '../aut
|
||||
|
||||
describe('auth UI URL helpers', () => {
|
||||
it('builds auth UI URLs under the configured auth base path', () => {
|
||||
expect(buildAuthUiUrl('https://auth.airi.build/ui', '/sign-in', '?client_id=web')).toBe(
|
||||
'https://auth.airi.build/ui/sign-in?client_id=web',
|
||||
expect(buildAuthUiUrl('https://accounts.airi.build/ui', '/sign-in', '?client_id=web')).toBe(
|
||||
'https://accounts.airi.build/ui/sign-in?client_id=web',
|
||||
)
|
||||
})
|
||||
|
||||
it('maps server /auth requests to the standalone auth UI while preserving queries', () => {
|
||||
expect(buildAuthUiRedirectUrl(
|
||||
'https://auth.airi.build/ui/',
|
||||
'https://accounts.airi.build/ui/',
|
||||
'https://api.airi.build/auth/verify-email?verified=true',
|
||||
)).toBe('https://auth.airi.build/ui/verify-email?verified=true')
|
||||
)).toBe('https://accounts.airi.build/ui/verify-email?verified=true')
|
||||
})
|
||||
|
||||
it('adds the API server origin for standalone auth UI cross-environment redirects', () => {
|
||||
@@ -28,7 +28,7 @@ describe('auth UI URL helpers', () => {
|
||||
|
||||
it('routes server-dev default auth UI redirects to the matching Pages branch', () => {
|
||||
expect(buildAuthUiRedirectUrl(
|
||||
'https://auth.airi.build/ui',
|
||||
'https://accounts.airi.build/ui',
|
||||
'https://airi-server-dev.up.railway.app/auth/sign-in?client_id=airi-stage-web',
|
||||
'https://airi-server-dev.up.railway.app',
|
||||
)).toBe(
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('origin utils', () => {
|
||||
})
|
||||
|
||||
it('allows the standalone auth and admin UI origins', () => {
|
||||
expect(getTrustedOrigin('https://auth.airi.build')).toBe('https://auth.airi.build')
|
||||
expect(getTrustedOrigin('https://accounts.airi.build')).toBe('https://accounts.airi.build')
|
||||
expect(getTrustedOrigin('https://server-dev.airi-server-auth.pages.dev')).toBe('https://server-dev.airi-server-auth.pages.dev')
|
||||
expect(getTrustedOrigin('https://admin.airi.build')).toBe('https://admin.airi.build')
|
||||
expect(getTrustedOrigin('https://server-dev.airi-server-admin.pages.dev')).toBe('https://server-dev.airi-server-admin.pages.dev')
|
||||
|
||||
@@ -40,6 +40,6 @@ Production expects:
|
||||
VITE_SERVER_URL=https://api.airi.build
|
||||
```
|
||||
|
||||
The server redirects historical `/auth/*` URLs to `AUTH_UI_URL`, which defaults to `https://auth.airi.build/ui`.
|
||||
The server redirects historical `/auth/*` URLs to `AUTH_UI_URL`, which defaults to `https://accounts.airi.build/ui`.
|
||||
|
||||
The `server-dev` workflow deploys a Cloudflare Pages branch build at `https://server-dev.moeru-ai-airi-auth.pages.dev/ui/` with `VITE_SERVER_URL=https://airi-server-dev.up.railway.app`. Set the server-dev API environment variable `AUTH_UI_URL=https://server-dev.moeru-ai-airi-auth.pages.dev/ui` when the full dev auth redirect chain should stay on server-dev.
|
||||
|
||||
@@ -7,17 +7,17 @@ import { getServerAuthBootstrapContext, resolveStandaloneServerAuthContext } fro
|
||||
describe('ui-server-auth bootstrap context', () => {
|
||||
it('uses the trusted API server origin carried by standalone server redirects', () => {
|
||||
expect(resolveStandaloneServerAuthContext(
|
||||
'https://auth.airi.build/ui/sign-in?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app%2Fapi%2Fauth&client_id=airi-stage-web',
|
||||
'https://accounts.airi.build/ui/sign-in?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app%2Fapi%2Fauth&client_id=airi-stage-web',
|
||||
'https://api.airi.build',
|
||||
)).toEqual({
|
||||
apiServerUrl: 'https://airi-server-dev.up.railway.app',
|
||||
currentUrl: 'https://auth.airi.build/ui/sign-in?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app%2Fapi%2Fauth&client_id=airi-stage-web',
|
||||
currentUrl: 'https://accounts.airi.build/ui/sign-in?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app%2Fapi%2Fauth&client_id=airi-stage-web',
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores untrusted API server origins from crafted standalone auth URLs', () => {
|
||||
expect(resolveStandaloneServerAuthContext(
|
||||
'https://auth.airi.build/ui/sign-in?api_server_url=https%3A%2F%2Fevil.example&client_id=airi-stage-web',
|
||||
'https://accounts.airi.build/ui/sign-in?api_server_url=https%3A%2F%2Fevil.example&client_id=airi-stage-web',
|
||||
'https://api.airi.build',
|
||||
)).toBeNull()
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@ export function getServerAuthBootstrapContext(): ServerAuthBootstrapContext | nu
|
||||
*
|
||||
* Use when:
|
||||
* - The standalone auth UI serves more than one AIRI environment from the same
|
||||
* Pages deployment, such as production `auth.airi.build` handling server-dev
|
||||
* Pages deployment, such as production `accounts.airi.build` handling server-dev
|
||||
* OIDC redirects.
|
||||
*
|
||||
* Expects:
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('useLinkedAccounts', () => {
|
||||
},
|
||||
isAuthenticated: ref(false),
|
||||
describeError: () => '',
|
||||
buildCallbackURL: () => 'https://auth.airi.build/ui/profile',
|
||||
buildCallbackURL: () => 'https://accounts.airi.build/ui/profile',
|
||||
messages: {
|
||||
listFailed: 'list failed',
|
||||
unlinkFailed: 'unlink failed',
|
||||
@@ -48,8 +48,8 @@ describe('useLinkedAccounts', () => {
|
||||
|
||||
expect(linkSocial).toHaveBeenCalledWith({
|
||||
provider: 'github',
|
||||
callbackURL: 'https://auth.airi.build/ui/profile',
|
||||
errorCallbackURL: 'https://auth.airi.build/ui/profile',
|
||||
callbackURL: 'https://accounts.airi.build/ui/profile',
|
||||
errorCallbackURL: 'https://accounts.airi.build/ui/profile',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user