diff --git a/.github/workflows/deploy-cloudflare-admin-ui.yml b/.github/workflows/deploy-cloudflare-admin-ui.yml index 6e39a1299..cc1bd49c2 100644 --- a/.github/workflows/deploy-cloudflare-admin-ui.yml +++ b/.github/workflows/deploy-cloudflare-admin-ui.yml @@ -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 diff --git a/.github/workflows/deploy-cloudflare-workers-dev-server.yml b/.github/workflows/deploy-cloudflare-workers-dev-server.yml index 91f6d4137..c409a73f0 100644 --- a/.github/workflows/deploy-cloudflare-workers-dev-server.yml +++ b/.github/workflows/deploy-cloudflare-workers-dev-server.yml @@ -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/" diff --git a/apps/server/README.md b/apps/server/README.md index 203f8cb28..ff6466b4e 100644 --- a/apps/server/README.md +++ b/apps/server/README.md @@ -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. diff --git a/apps/server/src/libs/env.ts b/apps/server/src/libs/env.ts index 0656ba16b..c6578044a 100644 --- a/apps/server/src/libs/env.ts +++ b/apps/server/src/libs/env.ts @@ -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 diff --git a/apps/server/src/libs/tests/auth.test.ts b/apps/server/src/libs/tests/auth.test.ts index 18693d4a2..5e1b0d392 100644 --- a/apps/server/src/libs/tests/auth.test.ts +++ b/apps/server/src/libs/tests/auth.test.ts @@ -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([[], [], []]) diff --git a/apps/server/src/libs/tests/env.test.ts b/apps/server/src/libs/tests/env.test.ts index 8b0a8c687..14b8b2924 100644 --- a/apps/server/src/libs/tests/env.test.ts +++ b/apps/server/src/libs/tests/env.test.ts @@ -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([]) }) diff --git a/apps/server/src/routes/auth/oidc-userinfo-ban.test.ts b/apps/server/src/routes/auth/oidc-userinfo-ban.test.ts index 74b3ec724..a326cec88 100644 --- a/apps/server/src/routes/auth/oidc-userinfo-ban.test.ts +++ b/apps/server/src/routes/auth/oidc-userinfo-ban.test.ts @@ -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 () => { diff --git a/apps/server/src/utils/server-admin-ui.ts b/apps/server/src/utils/server-admin-ui.ts index 5239c3cd4..c027696e0 100644 --- a/apps/server/src/utils/server-admin-ui.ts +++ b/apps/server/src/utils/server-admin-ui.ts @@ -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. diff --git a/apps/server/src/utils/tests/server-admin-ui.test.ts b/apps/server/src/utils/tests/server-admin-ui.test.ts index 20a05a98f..965934524 100644 --- a/apps/server/src/utils/tests/server-admin-ui.test.ts +++ b/apps/server/src/utils/tests/server-admin-ui.test.ts @@ -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') }) }) diff --git a/apps/ui-admin/README.md b/apps/ui-admin/README.md index 5cacdf5b6..cd7c93c43 100644 --- a/apps/ui-admin/README.md +++ b/apps/ui-admin/README.md @@ -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 diff --git a/apps/ui-admin/public/_redirects b/apps/ui-admin/public/_redirects index 4af6262b7..7797f7c6a 100644 --- a/apps/ui-admin/public/_redirects +++ b/apps/ui-admin/public/_redirects @@ -1,3 +1 @@ -/ /admin/ 302 -/admin /index.html 200 -/admin/* /index.html 200 +/* /index.html 200 diff --git a/apps/ui-admin/src/main.ts b/apps/ui-admin/src/main.ts index 01c8fb373..673b87ddf 100644 --- a/apps/ui-admin/src/main.ts +++ b/apps/ui-admin/src/main.ts @@ -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 }, diff --git a/apps/ui-admin/src/modules/server-admin-context.test.ts b/apps/ui-admin/src/modules/server-admin-context.test.ts index 6930065f4..46d7af1d6 100644 --- a/apps/ui-admin/src/modules/server-admin-context.test.ts +++ b/apps/ui-admin/src/modules/server-admin-context.test.ts @@ -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() }) diff --git a/apps/ui-admin/vite.config.ts b/apps/ui-admin/vite.config.ts index 0a141e704..413e2e75a 100644 --- a/apps/ui-admin/vite.config.ts +++ b/apps/ui-admin/vite.config.ts @@ -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')), diff --git a/apps/ui-server-auth/public/_redirects b/apps/ui-server-auth/public/_redirects index 060b172e1..e0706e420 100644 --- a/apps/ui-server-auth/public/_redirects +++ b/apps/ui-server-auth/public/_redirects @@ -1,3 +1,4 @@ +/ /ui/profile 302 /auth /ui/ 302 /auth/* /ui/:splat 302 /ui /index.html 200