feat(admin-ui): update admin UI URLs to remove trailing slashes and adjust redirects
This commit is contained in:
@@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
name: Admin Production
|
name: Admin Production
|
||||||
url: https://admin.airi.build/admin/
|
url: https://admin.airi.build/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ jobs:
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
name: AdminServerDev
|
name: AdminServerDev
|
||||||
url: https://server-dev.airi-server-admin.pages.dev/admin/
|
url: https://server-dev.airi-server-admin.pages.dev/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -176,4 +176,4 @@ jobs:
|
|||||||
|
|
||||||
- name: Print preview URL
|
- name: Print preview URL
|
||||||
run: |
|
run: |
|
||||||
echo "Preview URL: https://server-dev.airi-server-admin.pages.dev/admin/"
|
echo "Preview URL: https://server-dev.airi-server-admin.pages.dev/"
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Set this when previewing or deploying auth UI to a different Cloudflare URL.
|
|||||||
|
|
||||||
Default:
|
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.
|
Set this when previewing or deploying admin UI to a different Cloudflare URL.
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ const EnvSchema = object({
|
|||||||
// Standalone admin UI base URL. The server keeps `/admin/*` as the historical
|
// Standalone admin UI base URL. The server keeps `/admin/*` as the historical
|
||||||
// entrypoint and redirects those requests here after ui-admin moved out of
|
// entrypoint and redirects those requests here after ui-admin moved out of
|
||||||
// the server image.
|
// 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
|
// 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
|
// (success_url / cancel_url / portal return_url) when a request has no trusted
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import { ensureDynamicFirstPartyRedirectUri, seedTrustedClients } from '../auth'
|
import { createAuth, ensureDynamicFirstPartyRedirectUri, seedTrustedClients } from '../auth'
|
||||||
|
|
||||||
function createMockDb(existingRowsByCall: unknown[][] = []) {
|
function createMockDb(existingRowsByCall: unknown[][] = []) {
|
||||||
const limit = vi.fn()
|
const limit = vi.fn()
|
||||||
@@ -29,6 +29,22 @@ function createMockDb(existingRowsByCall: unknown[][] = []) {
|
|||||||
return { db, limit, values, capturedValues }
|
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', () => {
|
describe('seedTrustedClients', () => {
|
||||||
it('seeds trusted first-party clients with explicit oauth metadata', async () => {
|
it('seeds trusted first-party clients with explicit oauth metadata', async () => {
|
||||||
const { db, values, capturedValues } = createMockDb([[], [], []])
|
const { db, values, capturedValues } = createMockDb([[], [], []])
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe('parseEnv', () => {
|
|||||||
expect(env.DATABASE_URL).toBe('postgres://example')
|
expect(env.DATABASE_URL).toBe('postgres://example')
|
||||||
expect(env.REDIS_URL).toBe('redis://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://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([])
|
expect(env.ADDITIONAL_TRUSTED_ORIGINS).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ describe('auth UI routes', () => {
|
|||||||
expect(res.status).toBe(302)
|
expect(res.status).toBe(302)
|
||||||
|
|
||||||
const location = res.headers.get('location')
|
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 () => {
|
it('redirects Electron OIDC callback queries to the standalone auth UI relay', async () => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export const SERVER_ADMIN_UI_BASE_PATH = '/admin'
|
export const SERVER_ADMIN_UI_BASE_PATH = '/admin'
|
||||||
export const ADMIN_UI_API_SERVER_URL_QUERY_PARAM = 'api_server_url'
|
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_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.
|
* 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.
|
* - Preserving dashboard route paths and query parameters.
|
||||||
*
|
*
|
||||||
* Expects:
|
* 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.
|
* - `path` is the route path within the admin UI router.
|
||||||
*
|
*
|
||||||
* Returns:
|
* Returns:
|
||||||
@@ -75,7 +75,7 @@ export function resolveAdminUiUrl(adminUiUrl: string, apiServerUrl: string): str
|
|||||||
*
|
*
|
||||||
* Expects:
|
* Expects:
|
||||||
* - `requestUrl` is the incoming server URL.
|
* - `requestUrl` is the incoming server URL.
|
||||||
* - `adminUiUrl` points to the standalone admin UI base path.
|
* - `adminUiUrl` points to the standalone admin UI base.
|
||||||
*
|
*
|
||||||
* Returns:
|
* Returns:
|
||||||
* - The external admin UI URL preserving route suffix and query string.
|
* - 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', () => {
|
describe('admin UI URL helpers', () => {
|
||||||
it('builds admin UI URLs under the configured admin base path', () => {
|
it('builds admin UI URLs under the configured admin base path', () => {
|
||||||
expect(buildAdminUiUrl('https://admin.airi.build/admin', '/users', '?query=alice')).toBe(
|
expect(buildAdminUiUrl('https://admin.airi.build', '/users', '?query=alice')).toBe(
|
||||||
'https://admin.airi.build/admin/users?query=alice',
|
'https://admin.airi.build/users?query=alice',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('maps server /admin requests to the standalone admin UI while preserving queries', () => {
|
it('maps server /admin requests to the standalone admin UI while preserving queries', () => {
|
||||||
expect(buildAdminUiRedirectUrl(
|
expect(buildAdminUiRedirectUrl(
|
||||||
'https://admin.airi.build/admin/',
|
'https://admin.airi.build/',
|
||||||
'https://api.airi.build/admin/voice-packs?provider=openai',
|
'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', () => {
|
it('adds the API server origin for standalone admin UI cross-environment redirects', () => {
|
||||||
expect(buildAdminUiRedirectUrl(
|
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/admin/users?api_server_url=https%3A%2F%2Fevil.example',
|
||||||
'https://airi-server-dev.up.railway.app/api/admin',
|
'https://airi-server-dev.up.railway.app/api/admin',
|
||||||
)).toBe(
|
)).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', () => {
|
it('routes server-dev default admin UI redirects to the matching Pages branch', () => {
|
||||||
expect(buildAdminUiRedirectUrl(
|
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/admin/users?query=alice',
|
||||||
'https://airi-server-dev.up.railway.app',
|
'https://airi-server-dev.up.railway.app',
|
||||||
)).toBe(
|
)).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', () => {
|
it('keeps an explicitly configured admin UI URL for server-dev', () => {
|
||||||
expect(resolveAdminUiUrl(
|
expect(resolveAdminUiUrl(
|
||||||
'https://admin-preview.example/admin',
|
'https://admin-preview.example',
|
||||||
'https://airi-server-dev.up.railway.app',
|
'https://airi-server-dev.up.railway.app',
|
||||||
)).toBe('https://admin-preview.example/admin')
|
)).toBe('https://admin-preview.example')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# AIRI Admin Dashboard
|
# 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
|
## Use When
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
/ /admin/ 302
|
/* /index.html 200
|
||||||
/admin /index.html 200
|
|
||||||
/admin/* /index.html 200
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import './styles/main.css'
|
|||||||
import 'uno.css'
|
import 'uno.css'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory('/admin/'),
|
history: createWebHistory('/'),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: OverviewPage },
|
{ path: '/', component: OverviewPage },
|
||||||
{ path: '/users', component: UsersPage },
|
{ path: '/users', component: UsersPage },
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import { getServerAdminBootstrapContext, resolveStandaloneServerAdminContext } f
|
|||||||
describe('ui-admin bootstrap context', () => {
|
describe('ui-admin bootstrap context', () => {
|
||||||
it('uses the trusted API server origin carried by standalone server redirects', () => {
|
it('uses the trusted API server origin carried by standalone server redirects', () => {
|
||||||
expect(resolveStandaloneServerAdminContext(
|
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({
|
)).toEqual({
|
||||||
apiServerUrl: 'https://airi-server-dev.up.railway.app',
|
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', () => {
|
it('ignores untrusted API server origins from crafted standalone admin URLs', () => {
|
||||||
expect(resolveStandaloneServerAdminContext(
|
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()
|
)).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import VueMacros from 'vue-macros/vite'
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/',
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@proj-airi/stage-shared': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-shared', 'src')),
|
'@proj-airi/stage-shared': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-shared', 'src')),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/ /ui/profile 302
|
||||||
/auth /ui/ 302
|
/auth /ui/ 302
|
||||||
/auth/* /ui/:splat 302
|
/auth/* /ui/:splat 302
|
||||||
/ui /index.html 200
|
/ui /index.html 200
|
||||||
|
|||||||
Reference in New Issue
Block a user