diff --git a/src/api/modules/internal-surfaces.ts b/src/api/modules/internal-surfaces.ts new file mode 100644 index 0000000..838073e --- /dev/null +++ b/src/api/modules/internal-surfaces.ts @@ -0,0 +1,29 @@ +import { api } from '@/api/client' +import type { ApiResponse } from '@/api/types' + +/** + * Operator consoles that live outside Laravel (database admin, ...) but are + * gated by our own admin permissions. The backend registry is + * config/internal_surfaces.php; the surface id must match a key there. + */ +export type InternalSurface = 'db-studio' + +export interface InternalSurfaceSession { + kind: 'internal_surface_session' + surface: InternalSurface + label: string + /** single-use entry link — redeem it in the browser before it expires */ + url: string + expires_in: number +} + +/** + * Exchanges the admin bearer token for a single-use entry link. The link sets + * an HttpOnly session cookie on the surface's own hostname, so it must be + * opened in the browser rather than fetched. + */ +export function openInternalSurface(surface: InternalSurface) { + return api.post>( + `/api/admin/v1/internal-surfaces/${surface}/session`, + ) +} diff --git a/src/auth/permissions.ts b/src/auth/permissions.ts index ab183f3..684f588 100644 --- a/src/auth/permissions.ts +++ b/src/auth/permissions.ts @@ -31,6 +31,9 @@ export const PERM = { NOTIFICATION_READ: 'admin:notification:read', ROBOT_TOKEN_ISSUE: 'auth:robot-token:issue', API_DOCS_READ: 'auth:api-docs:read', + // internal operator consoles — `auth:` prefixed, so super-admin only + // (the `admin` role is synced to admin:% keys and would otherwise inherit it) + DB_STUDIO_ACCESS: 'auth:db-studio:access', // role management — only super-admin holds these ROLE_READ: 'auth:role:read', ROLE_CREATE: 'auth:role:create', diff --git a/src/components/layout/nav.ts b/src/components/layout/nav.ts index d303b2c..c58ec1d 100644 --- a/src/components/layout/nav.ts +++ b/src/components/layout/nav.ts @@ -4,6 +4,7 @@ import { BookOpenText, Bot, ClipboardList, + Database, FileText, FileJson, Flag, @@ -110,6 +111,12 @@ export const NAV_GROUPS: NavGroup[] = [ icon: FileJson, permission: 'auth:api-docs:read', }, + { + label: '数据库管理台', + to: '/db-studio', + icon: Database, + permission: 'auth:db-studio:access', + }, ], }, ] diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index a89dca2..0e826b6 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -9,38 +9,40 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as AuthedRouteImport } from './routes/_authed' -import { Route as ApiDocsRouteImport } from './routes/api-docs' import { Route as LoginRouteImport } from './routes/login' +import { Route as ApiDocsRouteImport } from './routes/api-docs' +import { Route as AuthedRouteImport } from './routes/_authed' import { Route as AuthedIndexRouteImport } from './routes/_authed/index' -import { Route as AuthedAdminUsersRouteImport } from './routes/_authed/admin-users' -import { Route as AuthedCommentReportsRouteImport } from './routes/_authed/comment-reports' -import { Route as AuthedDevicesRouteImport } from './routes/_authed/devices' -import { Route as AuthedDimzouTranslationsRouteImport } from './routes/_authed/dimzou-translations' -import { Route as AuthedEventReportsRouteImport } from './routes/_authed/event-reports' -import { Route as AuthedLocalesRouteImport } from './routes/_authed/locales' -import { Route as AuthedRobotsRouteImport } from './routes/_authed/robots' -import { Route as AuthedRolesRouteImport } from './routes/_authed/roles' import { Route as AuthedSidRouteImport } from './routes/_authed/sid' -import { Route as AuthedCategoriesIndexRouteImport } from './routes/_authed/categories/index' -import { Route as AuthedCategoriesPendingRouteImport } from './routes/_authed/categories/pending' -import { Route as AuthedDimzouDocumentsRouteImport } from './routes/_authed/dimzou/documents' -import { Route as AuthedDimzouPublicationsRouteImport } from './routes/_authed/dimzou/publications' -import { Route as AuthedExcDemandsRouteImport } from './routes/_authed/exc/demands' -import { Route as AuthedExcDispatchesRouteImport } from './routes/_authed/exc/dispatches' -import { Route as AuthedExcOrdersRouteImport } from './routes/_authed/exc/orders' -import { Route as AuthedExcProvidersRouteImport } from './routes/_authed/exc/providers' -import { Route as AuthedStudioAgentTokensRouteImport } from './routes/_authed/studio/agent-tokens' -import { Route as AuthedStudioArticlesRouteImport } from './routes/_authed/studio/articles' -import { Route as AuthedStudioCategoryBriefsRouteImport } from './routes/_authed/studio/category-briefs' -import { Route as AuthedStudioRobotAuthorsRouteImport } from './routes/_authed/studio/robot-authors' -import { Route as AuthedStudioSeriesRouteImport } from './routes/_authed/studio/series' -import { Route as AuthedStudioStylePresetsRouteImport } from './routes/_authed/studio/style-presets' -import { Route as AuthedStudioSubtopicsRouteImport } from './routes/_authed/studio/subtopics' +import { Route as AuthedRolesRouteImport } from './routes/_authed/roles' +import { Route as AuthedRobotsRouteImport } from './routes/_authed/robots' +import { Route as AuthedLocalesRouteImport } from './routes/_authed/locales' +import { Route as AuthedEventReportsRouteImport } from './routes/_authed/event-reports' +import { Route as AuthedDimzouTranslationsRouteImport } from './routes/_authed/dimzou-translations' +import { Route as AuthedDevicesRouteImport } from './routes/_authed/devices' +import { Route as AuthedDbStudioRouteImport } from './routes/_authed/db-studio' +import { Route as AuthedCommentReportsRouteImport } from './routes/_authed/comment-reports' +import { Route as AuthedAdminUsersRouteImport } from './routes/_authed/admin-users' import { Route as AuthedUsersIndexRouteImport } from './routes/_authed/users/index' +import { Route as AuthedCategoriesIndexRouteImport } from './routes/_authed/categories/index' +import { Route as AuthedStudioSubtopicsRouteImport } from './routes/_authed/studio/subtopics' +import { Route as AuthedStudioStylePresetsRouteImport } from './routes/_authed/studio/style-presets' +import { Route as AuthedStudioSeriesRouteImport } from './routes/_authed/studio/series' +import { Route as AuthedStudioRobotAuthorsRouteImport } from './routes/_authed/studio/robot-authors' +import { Route as AuthedStudioCategoryBriefsRouteImport } from './routes/_authed/studio/category-briefs' +import { Route as AuthedStudioArticlesRouteImport } from './routes/_authed/studio/articles' +import { Route as AuthedStudioAgentTokensRouteImport } from './routes/_authed/studio/agent-tokens' +import { Route as AuthedExcProvidersRouteImport } from './routes/_authed/exc/providers' +import { Route as AuthedExcOrdersRouteImport } from './routes/_authed/exc/orders' +import { Route as AuthedExcDispatchesRouteImport } from './routes/_authed/exc/dispatches' +import { Route as AuthedExcDemandsRouteImport } from './routes/_authed/exc/demands' +import { Route as AuthedDimzouPublicationsRouteImport } from './routes/_authed/dimzou/publications' +import { Route as AuthedDimzouDocumentsRouteImport } from './routes/_authed/dimzou/documents' +import { Route as AuthedCategoriesPendingRouteImport } from './routes/_authed/categories/pending' -const AuthedRoute = AuthedRouteImport.update({ - id: '/_authed', +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', getParentRoute: () => rootRouteImport, } as any) const ApiDocsRoute = ApiDocsRouteImport.update({ @@ -48,9 +50,8 @@ const ApiDocsRoute = ApiDocsRouteImport.update({ path: '/api-docs', getParentRoute: () => rootRouteImport, } as any) -const LoginRoute = LoginRouteImport.update({ - id: '/login', - path: '/login', +const AuthedRoute = AuthedRouteImport.update({ + id: '/_authed', getParentRoute: () => rootRouteImport, } as any) const AuthedIndexRoute = AuthedIndexRouteImport.update({ @@ -58,19 +59,29 @@ const AuthedIndexRoute = AuthedIndexRouteImport.update({ path: '/', getParentRoute: () => AuthedRoute, } as any) -const AuthedAdminUsersRoute = AuthedAdminUsersRouteImport.update({ - id: '/admin-users', - path: '/admin-users', +const AuthedSidRoute = AuthedSidRouteImport.update({ + id: '/sid', + path: '/sid', getParentRoute: () => AuthedRoute, } as any) -const AuthedCommentReportsRoute = AuthedCommentReportsRouteImport.update({ - id: '/comment-reports', - path: '/comment-reports', +const AuthedRolesRoute = AuthedRolesRouteImport.update({ + id: '/roles', + path: '/roles', getParentRoute: () => AuthedRoute, } as any) -const AuthedDevicesRoute = AuthedDevicesRouteImport.update({ - id: '/devices', - path: '/devices', +const AuthedRobotsRoute = AuthedRobotsRouteImport.update({ + id: '/robots', + path: '/robots', + getParentRoute: () => AuthedRoute, +} as any) +const AuthedLocalesRoute = AuthedLocalesRouteImport.update({ + id: '/locales', + path: '/locales', + getParentRoute: () => AuthedRoute, +} as any) +const AuthedEventReportsRoute = AuthedEventReportsRouteImport.update({ + id: '/event-reports', + path: '/event-reports', getParentRoute: () => AuthedRoute, } as any) const AuthedDimzouTranslationsRoute = @@ -79,29 +90,29 @@ const AuthedDimzouTranslationsRoute = path: '/dimzou-translations', getParentRoute: () => AuthedRoute, } as any) -const AuthedEventReportsRoute = AuthedEventReportsRouteImport.update({ - id: '/event-reports', - path: '/event-reports', +const AuthedDevicesRoute = AuthedDevicesRouteImport.update({ + id: '/devices', + path: '/devices', getParentRoute: () => AuthedRoute, } as any) -const AuthedLocalesRoute = AuthedLocalesRouteImport.update({ - id: '/locales', - path: '/locales', +const AuthedDbStudioRoute = AuthedDbStudioRouteImport.update({ + id: '/db-studio', + path: '/db-studio', getParentRoute: () => AuthedRoute, } as any) -const AuthedRobotsRoute = AuthedRobotsRouteImport.update({ - id: '/robots', - path: '/robots', +const AuthedCommentReportsRoute = AuthedCommentReportsRouteImport.update({ + id: '/comment-reports', + path: '/comment-reports', getParentRoute: () => AuthedRoute, } as any) -const AuthedRolesRoute = AuthedRolesRouteImport.update({ - id: '/roles', - path: '/roles', +const AuthedAdminUsersRoute = AuthedAdminUsersRouteImport.update({ + id: '/admin-users', + path: '/admin-users', getParentRoute: () => AuthedRoute, } as any) -const AuthedSidRoute = AuthedSidRouteImport.update({ - id: '/sid', - path: '/sid', +const AuthedUsersIndexRoute = AuthedUsersIndexRouteImport.update({ + id: '/users/', + path: '/users/', getParentRoute: () => AuthedRoute, } as any) const AuthedCategoriesIndexRoute = AuthedCategoriesIndexRouteImport.update({ @@ -109,67 +120,9 @@ const AuthedCategoriesIndexRoute = AuthedCategoriesIndexRouteImport.update({ path: '/categories/', getParentRoute: () => AuthedRoute, } as any) -const AuthedCategoriesPendingRoute = AuthedCategoriesPendingRouteImport.update({ - id: '/categories/pending', - path: '/categories/pending', - getParentRoute: () => AuthedRoute, -} as any) -const AuthedDimzouDocumentsRoute = AuthedDimzouDocumentsRouteImport.update({ - id: '/dimzou/documents', - path: '/dimzou/documents', - getParentRoute: () => AuthedRoute, -} as any) -const AuthedDimzouPublicationsRoute = - AuthedDimzouPublicationsRouteImport.update({ - id: '/dimzou/publications', - path: '/dimzou/publications', - getParentRoute: () => AuthedRoute, - } as any) -const AuthedExcDemandsRoute = AuthedExcDemandsRouteImport.update({ - id: '/exc/demands', - path: '/exc/demands', - getParentRoute: () => AuthedRoute, -} as any) -const AuthedExcDispatchesRoute = AuthedExcDispatchesRouteImport.update({ - id: '/exc/dispatches', - path: '/exc/dispatches', - getParentRoute: () => AuthedRoute, -} as any) -const AuthedExcOrdersRoute = AuthedExcOrdersRouteImport.update({ - id: '/exc/orders', - path: '/exc/orders', - getParentRoute: () => AuthedRoute, -} as any) -const AuthedExcProvidersRoute = AuthedExcProvidersRouteImport.update({ - id: '/exc/providers', - path: '/exc/providers', - getParentRoute: () => AuthedRoute, -} as any) -const AuthedStudioAgentTokensRoute = AuthedStudioAgentTokensRouteImport.update({ - id: '/studio/agent-tokens', - path: '/studio/agent-tokens', - getParentRoute: () => AuthedRoute, -} as any) -const AuthedStudioArticlesRoute = AuthedStudioArticlesRouteImport.update({ - id: '/studio/articles', - path: '/studio/articles', - getParentRoute: () => AuthedRoute, -} as any) -const AuthedStudioCategoryBriefsRoute = - AuthedStudioCategoryBriefsRouteImport.update({ - id: '/studio/category-briefs', - path: '/studio/category-briefs', - getParentRoute: () => AuthedRoute, - } as any) -const AuthedStudioRobotAuthorsRoute = - AuthedStudioRobotAuthorsRouteImport.update({ - id: '/studio/robot-authors', - path: '/studio/robot-authors', - getParentRoute: () => AuthedRoute, - } as any) -const AuthedStudioSeriesRoute = AuthedStudioSeriesRouteImport.update({ - id: '/studio/series', - path: '/studio/series', +const AuthedStudioSubtopicsRoute = AuthedStudioSubtopicsRouteImport.update({ + id: '/studio/subtopics', + path: '/studio/subtopics', getParentRoute: () => AuthedRoute, } as any) const AuthedStudioStylePresetsRoute = @@ -178,14 +131,67 @@ const AuthedStudioStylePresetsRoute = path: '/studio/style-presets', getParentRoute: () => AuthedRoute, } as any) -const AuthedStudioSubtopicsRoute = AuthedStudioSubtopicsRouteImport.update({ - id: '/studio/subtopics', - path: '/studio/subtopics', +const AuthedStudioSeriesRoute = AuthedStudioSeriesRouteImport.update({ + id: '/studio/series', + path: '/studio/series', getParentRoute: () => AuthedRoute, } as any) -const AuthedUsersIndexRoute = AuthedUsersIndexRouteImport.update({ - id: '/users/', - path: '/users/', +const AuthedStudioRobotAuthorsRoute = + AuthedStudioRobotAuthorsRouteImport.update({ + id: '/studio/robot-authors', + path: '/studio/robot-authors', + getParentRoute: () => AuthedRoute, + } as any) +const AuthedStudioCategoryBriefsRoute = + AuthedStudioCategoryBriefsRouteImport.update({ + id: '/studio/category-briefs', + path: '/studio/category-briefs', + getParentRoute: () => AuthedRoute, + } as any) +const AuthedStudioArticlesRoute = AuthedStudioArticlesRouteImport.update({ + id: '/studio/articles', + path: '/studio/articles', + getParentRoute: () => AuthedRoute, +} as any) +const AuthedStudioAgentTokensRoute = AuthedStudioAgentTokensRouteImport.update({ + id: '/studio/agent-tokens', + path: '/studio/agent-tokens', + getParentRoute: () => AuthedRoute, +} as any) +const AuthedExcProvidersRoute = AuthedExcProvidersRouteImport.update({ + id: '/exc/providers', + path: '/exc/providers', + getParentRoute: () => AuthedRoute, +} as any) +const AuthedExcOrdersRoute = AuthedExcOrdersRouteImport.update({ + id: '/exc/orders', + path: '/exc/orders', + getParentRoute: () => AuthedRoute, +} as any) +const AuthedExcDispatchesRoute = AuthedExcDispatchesRouteImport.update({ + id: '/exc/dispatches', + path: '/exc/dispatches', + getParentRoute: () => AuthedRoute, +} as any) +const AuthedExcDemandsRoute = AuthedExcDemandsRouteImport.update({ + id: '/exc/demands', + path: '/exc/demands', + getParentRoute: () => AuthedRoute, +} as any) +const AuthedDimzouPublicationsRoute = + AuthedDimzouPublicationsRouteImport.update({ + id: '/dimzou/publications', + path: '/dimzou/publications', + getParentRoute: () => AuthedRoute, + } as any) +const AuthedDimzouDocumentsRoute = AuthedDimzouDocumentsRouteImport.update({ + id: '/dimzou/documents', + path: '/dimzou/documents', + getParentRoute: () => AuthedRoute, +} as any) +const AuthedCategoriesPendingRoute = AuthedCategoriesPendingRouteImport.update({ + id: '/categories/pending', + path: '/categories/pending', getParentRoute: () => AuthedRoute, } as any) @@ -195,6 +201,7 @@ export interface FileRoutesByFullPath { '/login': typeof LoginRoute '/admin-users': typeof AuthedAdminUsersRoute '/comment-reports': typeof AuthedCommentReportsRoute + '/db-studio': typeof AuthedDbStudioRoute '/devices': typeof AuthedDevicesRoute '/dimzou-translations': typeof AuthedDimzouTranslationsRoute '/event-reports': typeof AuthedEventReportsRoute @@ -224,6 +231,7 @@ export interface FileRoutesByTo { '/login': typeof LoginRoute '/admin-users': typeof AuthedAdminUsersRoute '/comment-reports': typeof AuthedCommentReportsRoute + '/db-studio': typeof AuthedDbStudioRoute '/devices': typeof AuthedDevicesRoute '/dimzou-translations': typeof AuthedDimzouTranslationsRoute '/event-reports': typeof AuthedEventReportsRoute @@ -256,6 +264,7 @@ export interface FileRoutesById { '/login': typeof LoginRoute '/_authed/admin-users': typeof AuthedAdminUsersRoute '/_authed/comment-reports': typeof AuthedCommentReportsRoute + '/_authed/db-studio': typeof AuthedDbStudioRoute '/_authed/devices': typeof AuthedDevicesRoute '/_authed/dimzou-translations': typeof AuthedDimzouTranslationsRoute '/_authed/event-reports': typeof AuthedEventReportsRoute @@ -289,6 +298,7 @@ export interface FileRouteTypes { | '/login' | '/admin-users' | '/comment-reports' + | '/db-studio' | '/devices' | '/dimzou-translations' | '/event-reports' @@ -318,6 +328,7 @@ export interface FileRouteTypes { | '/login' | '/admin-users' | '/comment-reports' + | '/db-studio' | '/devices' | '/dimzou-translations' | '/event-reports' @@ -349,6 +360,7 @@ export interface FileRouteTypes { | '/login' | '/_authed/admin-users' | '/_authed/comment-reports' + | '/_authed/db-studio' | '/_authed/devices' | '/_authed/dimzou-translations' | '/_authed/event-reports' @@ -383,11 +395,11 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/_authed': { - id: '/_authed' - path: '' - fullPath: '/' - preLoaderRoute: typeof AuthedRouteImport + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport parentRoute: typeof rootRouteImport } '/api-docs': { @@ -397,11 +409,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiDocsRouteImport parentRoute: typeof rootRouteImport } - '/login': { - id: '/login' - path: '/login' - fullPath: '/login' - preLoaderRoute: typeof LoginRouteImport + '/_authed': { + id: '/_authed' + path: '' + fullPath: '/' + preLoaderRoute: typeof AuthedRouteImport parentRoute: typeof rootRouteImport } '/_authed/': { @@ -411,53 +423,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedIndexRouteImport parentRoute: typeof AuthedRoute } - '/_authed/admin-users': { - id: '/_authed/admin-users' - path: '/admin-users' - fullPath: '/admin-users' - preLoaderRoute: typeof AuthedAdminUsersRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/comment-reports': { - id: '/_authed/comment-reports' - path: '/comment-reports' - fullPath: '/comment-reports' - preLoaderRoute: typeof AuthedCommentReportsRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/devices': { - id: '/_authed/devices' - path: '/devices' - fullPath: '/devices' - preLoaderRoute: typeof AuthedDevicesRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/dimzou-translations': { - id: '/_authed/dimzou-translations' - path: '/dimzou-translations' - fullPath: '/dimzou-translations' - preLoaderRoute: typeof AuthedDimzouTranslationsRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/event-reports': { - id: '/_authed/event-reports' - path: '/event-reports' - fullPath: '/event-reports' - preLoaderRoute: typeof AuthedEventReportsRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/locales': { - id: '/_authed/locales' - path: '/locales' - fullPath: '/locales' - preLoaderRoute: typeof AuthedLocalesRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/robots': { - id: '/_authed/robots' - path: '/robots' - fullPath: '/robots' - preLoaderRoute: typeof AuthedRobotsRouteImport + '/_authed/sid': { + id: '/_authed/sid' + path: '/sid' + fullPath: '/sid' + preLoaderRoute: typeof AuthedSidRouteImport parentRoute: typeof AuthedRoute } '/_authed/roles': { @@ -467,116 +437,60 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedRolesRouteImport parentRoute: typeof AuthedRoute } - '/_authed/sid': { - id: '/_authed/sid' - path: '/sid' - fullPath: '/sid' - preLoaderRoute: typeof AuthedSidRouteImport + '/_authed/robots': { + id: '/_authed/robots' + path: '/robots' + fullPath: '/robots' + preLoaderRoute: typeof AuthedRobotsRouteImport parentRoute: typeof AuthedRoute } - '/_authed/categories/': { - id: '/_authed/categories/' - path: '/categories' - fullPath: '/categories/' - preLoaderRoute: typeof AuthedCategoriesIndexRouteImport + '/_authed/locales': { + id: '/_authed/locales' + path: '/locales' + fullPath: '/locales' + preLoaderRoute: typeof AuthedLocalesRouteImport parentRoute: typeof AuthedRoute } - '/_authed/categories/pending': { - id: '/_authed/categories/pending' - path: '/categories/pending' - fullPath: '/categories/pending' - preLoaderRoute: typeof AuthedCategoriesPendingRouteImport + '/_authed/event-reports': { + id: '/_authed/event-reports' + path: '/event-reports' + fullPath: '/event-reports' + preLoaderRoute: typeof AuthedEventReportsRouteImport parentRoute: typeof AuthedRoute } - '/_authed/dimzou/documents': { - id: '/_authed/dimzou/documents' - path: '/dimzou/documents' - fullPath: '/dimzou/documents' - preLoaderRoute: typeof AuthedDimzouDocumentsRouteImport + '/_authed/dimzou-translations': { + id: '/_authed/dimzou-translations' + path: '/dimzou-translations' + fullPath: '/dimzou-translations' + preLoaderRoute: typeof AuthedDimzouTranslationsRouteImport parentRoute: typeof AuthedRoute } - '/_authed/dimzou/publications': { - id: '/_authed/dimzou/publications' - path: '/dimzou/publications' - fullPath: '/dimzou/publications' - preLoaderRoute: typeof AuthedDimzouPublicationsRouteImport + '/_authed/devices': { + id: '/_authed/devices' + path: '/devices' + fullPath: '/devices' + preLoaderRoute: typeof AuthedDevicesRouteImport parentRoute: typeof AuthedRoute } - '/_authed/exc/demands': { - id: '/_authed/exc/demands' - path: '/exc/demands' - fullPath: '/exc/demands' - preLoaderRoute: typeof AuthedExcDemandsRouteImport + '/_authed/db-studio': { + id: '/_authed/db-studio' + path: '/db-studio' + fullPath: '/db-studio' + preLoaderRoute: typeof AuthedDbStudioRouteImport parentRoute: typeof AuthedRoute } - '/_authed/exc/dispatches': { - id: '/_authed/exc/dispatches' - path: '/exc/dispatches' - fullPath: '/exc/dispatches' - preLoaderRoute: typeof AuthedExcDispatchesRouteImport + '/_authed/comment-reports': { + id: '/_authed/comment-reports' + path: '/comment-reports' + fullPath: '/comment-reports' + preLoaderRoute: typeof AuthedCommentReportsRouteImport parentRoute: typeof AuthedRoute } - '/_authed/exc/orders': { - id: '/_authed/exc/orders' - path: '/exc/orders' - fullPath: '/exc/orders' - preLoaderRoute: typeof AuthedExcOrdersRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/exc/providers': { - id: '/_authed/exc/providers' - path: '/exc/providers' - fullPath: '/exc/providers' - preLoaderRoute: typeof AuthedExcProvidersRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/studio/agent-tokens': { - id: '/_authed/studio/agent-tokens' - path: '/studio/agent-tokens' - fullPath: '/studio/agent-tokens' - preLoaderRoute: typeof AuthedStudioAgentTokensRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/studio/articles': { - id: '/_authed/studio/articles' - path: '/studio/articles' - fullPath: '/studio/articles' - preLoaderRoute: typeof AuthedStudioArticlesRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/studio/category-briefs': { - id: '/_authed/studio/category-briefs' - path: '/studio/category-briefs' - fullPath: '/studio/category-briefs' - preLoaderRoute: typeof AuthedStudioCategoryBriefsRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/studio/robot-authors': { - id: '/_authed/studio/robot-authors' - path: '/studio/robot-authors' - fullPath: '/studio/robot-authors' - preLoaderRoute: typeof AuthedStudioRobotAuthorsRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/studio/series': { - id: '/_authed/studio/series' - path: '/studio/series' - fullPath: '/studio/series' - preLoaderRoute: typeof AuthedStudioSeriesRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/studio/style-presets': { - id: '/_authed/studio/style-presets' - path: '/studio/style-presets' - fullPath: '/studio/style-presets' - preLoaderRoute: typeof AuthedStudioStylePresetsRouteImport - parentRoute: typeof AuthedRoute - } - '/_authed/studio/subtopics': { - id: '/_authed/studio/subtopics' - path: '/studio/subtopics' - fullPath: '/studio/subtopics' - preLoaderRoute: typeof AuthedStudioSubtopicsRouteImport + '/_authed/admin-users': { + id: '/_authed/admin-users' + path: '/admin-users' + fullPath: '/admin-users' + preLoaderRoute: typeof AuthedAdminUsersRouteImport parentRoute: typeof AuthedRoute } '/_authed/users/': { @@ -586,12 +500,118 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedUsersIndexRouteImport parentRoute: typeof AuthedRoute } + '/_authed/categories/': { + id: '/_authed/categories/' + path: '/categories' + fullPath: '/categories/' + preLoaderRoute: typeof AuthedCategoriesIndexRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/studio/subtopics': { + id: '/_authed/studio/subtopics' + path: '/studio/subtopics' + fullPath: '/studio/subtopics' + preLoaderRoute: typeof AuthedStudioSubtopicsRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/studio/style-presets': { + id: '/_authed/studio/style-presets' + path: '/studio/style-presets' + fullPath: '/studio/style-presets' + preLoaderRoute: typeof AuthedStudioStylePresetsRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/studio/series': { + id: '/_authed/studio/series' + path: '/studio/series' + fullPath: '/studio/series' + preLoaderRoute: typeof AuthedStudioSeriesRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/studio/robot-authors': { + id: '/_authed/studio/robot-authors' + path: '/studio/robot-authors' + fullPath: '/studio/robot-authors' + preLoaderRoute: typeof AuthedStudioRobotAuthorsRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/studio/category-briefs': { + id: '/_authed/studio/category-briefs' + path: '/studio/category-briefs' + fullPath: '/studio/category-briefs' + preLoaderRoute: typeof AuthedStudioCategoryBriefsRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/studio/articles': { + id: '/_authed/studio/articles' + path: '/studio/articles' + fullPath: '/studio/articles' + preLoaderRoute: typeof AuthedStudioArticlesRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/studio/agent-tokens': { + id: '/_authed/studio/agent-tokens' + path: '/studio/agent-tokens' + fullPath: '/studio/agent-tokens' + preLoaderRoute: typeof AuthedStudioAgentTokensRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/exc/providers': { + id: '/_authed/exc/providers' + path: '/exc/providers' + fullPath: '/exc/providers' + preLoaderRoute: typeof AuthedExcProvidersRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/exc/orders': { + id: '/_authed/exc/orders' + path: '/exc/orders' + fullPath: '/exc/orders' + preLoaderRoute: typeof AuthedExcOrdersRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/exc/dispatches': { + id: '/_authed/exc/dispatches' + path: '/exc/dispatches' + fullPath: '/exc/dispatches' + preLoaderRoute: typeof AuthedExcDispatchesRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/exc/demands': { + id: '/_authed/exc/demands' + path: '/exc/demands' + fullPath: '/exc/demands' + preLoaderRoute: typeof AuthedExcDemandsRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/dimzou/publications': { + id: '/_authed/dimzou/publications' + path: '/dimzou/publications' + fullPath: '/dimzou/publications' + preLoaderRoute: typeof AuthedDimzouPublicationsRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/dimzou/documents': { + id: '/_authed/dimzou/documents' + path: '/dimzou/documents' + fullPath: '/dimzou/documents' + preLoaderRoute: typeof AuthedDimzouDocumentsRouteImport + parentRoute: typeof AuthedRoute + } + '/_authed/categories/pending': { + id: '/_authed/categories/pending' + path: '/categories/pending' + fullPath: '/categories/pending' + preLoaderRoute: typeof AuthedCategoriesPendingRouteImport + parentRoute: typeof AuthedRoute + } } } interface AuthedRouteChildren { AuthedAdminUsersRoute: typeof AuthedAdminUsersRoute AuthedCommentReportsRoute: typeof AuthedCommentReportsRoute + AuthedDbStudioRoute: typeof AuthedDbStudioRoute AuthedDevicesRoute: typeof AuthedDevicesRoute AuthedDimzouTranslationsRoute: typeof AuthedDimzouTranslationsRoute AuthedEventReportsRoute: typeof AuthedEventReportsRoute @@ -621,6 +641,7 @@ interface AuthedRouteChildren { const AuthedRouteChildren: AuthedRouteChildren = { AuthedAdminUsersRoute: AuthedAdminUsersRoute, AuthedCommentReportsRoute: AuthedCommentReportsRoute, + AuthedDbStudioRoute: AuthedDbStudioRoute, AuthedDevicesRoute: AuthedDevicesRoute, AuthedDimzouTranslationsRoute: AuthedDimzouTranslationsRoute, AuthedEventReportsRoute: AuthedEventReportsRoute, diff --git a/src/routes/_authed/db-studio.tsx b/src/routes/_authed/db-studio.tsx new file mode 100644 index 0000000..61b66c7 --- /dev/null +++ b/src/routes/_authed/db-studio.tsx @@ -0,0 +1,69 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useState } from 'react' +import { openInternalSurface } from '@/api/modules/internal-surfaces' +import { PERM, requirePermission } from '@/auth/permissions' +import { handleApiError } from '@/lib/errors' +import { PageHeader } from '@/components/page-header' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' + +export const Route = createFileRoute('/_authed/db-studio')({ + beforeLoad: () => requirePermission(PERM.DB_STUDIO_ACCESS), + component: DbStudioPage, +}) + +function DbStudioPage() { + const [busy, setBusy] = useState(false) + + const open = async () => { + setBusy(true) + + // The tab must be opened synchronously inside the click handler, or the + // popup blocker kills it once the request resolves. `noopener` is not + // usable here (it makes window.open return null), so detach manually. + const tab = window.open('about:blank', '_blank') + + try { + const res = await openInternalSurface('db-studio') + if (tab) { + tab.opener = null + tab.location.replace(res.data.url) + } else { + // popup blocked — fall back to navigating this tab + window.location.assign(res.data.url) + } + } catch (error) { + tab?.close() + handleApiError(error) + } finally { + setBusy(false) + } + } + + return ( +
+ + + + 打开管理台 + + 将在新标签页中打开。入口链接一次性有效、60 秒内过期,会话约 30 + 分钟后失效,重新打开即可。 + + + +

+ 管理台以数据库属主身份执行 SQL,操作不可撤销,且不会经过应用层的校验与审计。 + 请优先使用后台既有功能,仅在排查问题时使用本工具。 +

+ +
+
+
+ ) +}