feat(admin-ui): update admin UI URLs to remove trailing slashes and adjust redirects

This commit is contained in:
RainbowBird
2026-06-09 17:46:58 +08:00
parent 2ac4e90d56
commit 7fa7f8a072
15 changed files with 45 additions and 31 deletions
@@ -21,7 +21,7 @@ jobs:
environment:
name: Admin Production
url: https://admin.airi.build/admin/
url: https://admin.airi.build/
steps:
- uses: actions/checkout@v6
@@ -140,7 +140,7 @@ jobs:
environment:
name: AdminServerDev
url: https://server-dev.airi-server-admin.pages.dev/admin/
url: https://server-dev.airi-server-admin.pages.dev/
steps:
- uses: actions/checkout@v6
@@ -176,4 +176,4 @@ jobs:
- name: Print preview URL
run: |
echo "Preview URL: https://server-dev.airi-server-admin.pages.dev/admin/"
echo "Preview URL: https://server-dev.airi-server-admin.pages.dev/"
+1 -1
View File
@@ -42,7 +42,7 @@ Set this when previewing or deploying auth UI to a different Cloudflare URL.
Default:
`ADMIN_UI_URL=https://admin.airi.build/admin`
`ADMIN_UI_URL=https://admin.airi.build`
Set this when previewing or deploying admin UI to a different Cloudflare URL.
+1 -1
View File
@@ -88,7 +88,7 @@ const EnvSchema = object({
// Standalone admin UI base URL. The server keeps `/admin/*` as the historical
// entrypoint and redirects those requests here after ui-admin moved out of
// the server image.
ADMIN_UI_URL: optional(string(), 'https://admin.airi.build/admin'),
ADMIN_UI_URL: optional(string(), 'https://admin.airi.build'),
// Canonical user-facing web app origin. Used as the Stripe redirect base
// (success_url / cancel_url / portal return_url) when a request has no trusted
+17 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { ensureDynamicFirstPartyRedirectUri, seedTrustedClients } from '../auth'
import { createAuth, ensureDynamicFirstPartyRedirectUri, seedTrustedClients } from '../auth'
function createMockDb(existingRowsByCall: unknown[][] = []) {
const limit = vi.fn()
@@ -29,6 +29,22 @@ function createMockDb(existingRowsByCall: unknown[][] = []) {
return { db, limit, values, capturedValues }
}
describe('createAuth', () => {
it('allows signed-in users to link OAuth accounts that use a different email', () => {
const auth = createAuth({} as any, {
API_SERVER_URL: 'http://localhost:3000',
AUTH_GOOGLE_CLIENT_ID: 'google-client',
AUTH_GOOGLE_CLIENT_SECRET: 'google-secret',
AUTH_GITHUB_CLIENT_ID: 'github-client',
AUTH_GITHUB_CLIENT_SECRET: 'github-secret',
BETTER_AUTH_SECRET: 'test-secret-test-secret-test-secret',
ADDITIONAL_TRUSTED_ORIGINS: [],
} as any)
expect(auth.options.account?.accountLinking?.allowDifferentEmails).toBe(true)
})
})
describe('seedTrustedClients', () => {
it('seeds trusted first-party clients with explicit oauth metadata', async () => {
const { db, values, capturedValues } = createMockDb([[], [], []])
+1 -1
View File
@@ -42,7 +42,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.ADMIN_UI_URL).toBe('https://admin.airi.build/admin')
expect(env.ADMIN_UI_URL).toBe('https://admin.airi.build')
expect(env.ADDITIONAL_TRUSTED_ORIGINS).toEqual([])
})
@@ -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')
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')
})
it('redirects Electron OIDC callback queries to the standalone auth UI relay', async () => {
+4 -4
View File
@@ -1,8 +1,8 @@
export const SERVER_ADMIN_UI_BASE_PATH = '/admin'
export const ADMIN_UI_API_SERVER_URL_QUERY_PARAM = 'api_server_url'
export const DEFAULT_ADMIN_UI_URL = 'https://admin.airi.build/admin'
export const DEFAULT_ADMIN_UI_URL = 'https://admin.airi.build'
export const SERVER_DEV_API_SERVER_URL = 'https://airi-server-dev.up.railway.app'
export const SERVER_DEV_ADMIN_UI_URL = 'https://server-dev.airi-server-admin.pages.dev/admin'
export const SERVER_DEV_ADMIN_UI_URL = 'https://server-dev.airi-server-admin.pages.dev'
/**
* Builds an absolute URL inside the externally hosted admin UI.
@@ -13,7 +13,7 @@ export const SERVER_DEV_ADMIN_UI_URL = 'https://server-dev.airi-server-admin.pag
* - Preserving dashboard route paths and query parameters.
*
* Expects:
* - `adminUiUrl` is the public admin UI base, usually ending in `/admin`.
* - `adminUiUrl` is the public admin UI base.
* - `path` is the route path within the admin UI router.
*
* Returns:
@@ -75,7 +75,7 @@ export function resolveAdminUiUrl(adminUiUrl: string, apiServerUrl: string): str
*
* Expects:
* - `requestUrl` is the incoming server URL.
* - `adminUiUrl` points to the standalone admin UI base path.
* - `adminUiUrl` points to the standalone admin UI base.
*
* Returns:
* - The external admin UI URL preserving route suffix and query string.
@@ -4,42 +4,42 @@ import { buildAdminUiRedirectUrl, buildAdminUiUrl, resolveAdminUiUrl } from '../
describe('admin UI URL helpers', () => {
it('builds admin UI URLs under the configured admin base path', () => {
expect(buildAdminUiUrl('https://admin.airi.build/admin', '/users', '?query=alice')).toBe(
'https://admin.airi.build/admin/users?query=alice',
expect(buildAdminUiUrl('https://admin.airi.build', '/users', '?query=alice')).toBe(
'https://admin.airi.build/users?query=alice',
)
})
it('maps server /admin requests to the standalone admin UI while preserving queries', () => {
expect(buildAdminUiRedirectUrl(
'https://admin.airi.build/admin/',
'https://admin.airi.build/',
'https://api.airi.build/admin/voice-packs?provider=openai',
)).toBe('https://admin.airi.build/admin/voice-packs?provider=openai')
)).toBe('https://admin.airi.build/voice-packs?provider=openai')
})
it('adds the API server origin for standalone admin UI cross-environment redirects', () => {
expect(buildAdminUiRedirectUrl(
'https://admin-preview.example/admin/',
'https://admin-preview.example/',
'https://airi-server-dev.up.railway.app/admin/users?api_server_url=https%3A%2F%2Fevil.example',
'https://airi-server-dev.up.railway.app/api/admin',
)).toBe(
'https://admin-preview.example/admin/users?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app',
'https://admin-preview.example/users?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app',
)
})
it('routes server-dev default admin UI redirects to the matching Pages branch', () => {
expect(buildAdminUiRedirectUrl(
'https://admin.airi.build/admin',
'https://admin.airi.build',
'https://airi-server-dev.up.railway.app/admin/users?query=alice',
'https://airi-server-dev.up.railway.app',
)).toBe(
'https://server-dev.airi-server-admin.pages.dev/admin/users?query=alice&api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app',
'https://server-dev.airi-server-admin.pages.dev/users?query=alice&api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app',
)
})
it('keeps an explicitly configured admin UI URL for server-dev', () => {
expect(resolveAdminUiUrl(
'https://admin-preview.example/admin',
'https://admin-preview.example',
'https://airi-server-dev.up.railway.app',
)).toBe('https://admin-preview.example/admin')
)).toBe('https://admin-preview.example')
})
})
+1 -1
View File
@@ -1,6 +1,6 @@
# AIRI Admin Dashboard
Admin dashboard for operating the hosted AIRI server. It is a standalone Vue/Vite app deployed to Cloudflare Pages under `/admin`; the API server redirects its historical `/admin/*` entrypoints to this app.
Admin dashboard for operating the hosted AIRI server. It is a standalone Vue/Vite app deployed to Cloudflare Pages; the API server redirects its historical `/admin/*` entrypoints to this app.
## Use When
+1 -3
View File
@@ -1,3 +1 @@
/ /admin/ 302
/admin /index.html 200
/admin/* /index.html 200
/* /index.html 200
+1 -1
View File
@@ -20,7 +20,7 @@ import './styles/main.css'
import 'uno.css'
const router = createRouter({
history: createWebHistory('/admin/'),
history: createWebHistory('/'),
routes: [
{ path: '/', component: OverviewPage },
{ path: '/users', component: UsersPage },
@@ -7,16 +7,16 @@ import { getServerAdminBootstrapContext, resolveStandaloneServerAdminContext } f
describe('ui-admin bootstrap context', () => {
it('uses the trusted API server origin carried by standalone server redirects', () => {
expect(resolveStandaloneServerAdminContext(
'https://admin.airi.build/admin/users?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app%2Fapi%2Fadmin',
'https://admin.airi.build/users?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app%2Fapi%2Fadmin',
)).toEqual({
apiServerUrl: 'https://airi-server-dev.up.railway.app',
currentUrl: 'https://admin.airi.build/admin/users?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app%2Fapi%2Fadmin',
currentUrl: 'https://admin.airi.build/users?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app%2Fapi%2Fadmin',
})
})
it('ignores untrusted API server origins from crafted standalone admin URLs', () => {
expect(resolveStandaloneServerAdminContext(
'https://admin.airi.build/admin/users?api_server_url=https%3A%2F%2Fevil.example',
'https://admin.airi.build/users?api_server_url=https%3A%2F%2Fevil.example',
)).toBeNull()
})
-1
View File
@@ -7,7 +7,6 @@ import VueMacros from 'vue-macros/vite'
import { defineConfig } from 'vite'
export default defineConfig({
base: '/',
resolve: {
alias: {
'@proj-airi/stage-shared': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-shared', 'src')),
+1
View File
@@ -1,3 +1,4 @@
/ /ui/profile 302
/auth /ui/ 302
/auth/* /ui/:splat 302
/ui /index.html 200