feat(db-studio): add gated entry to the database console
Adds 开发工具 → 数据库管理台, which exchanges the admin bearer token for a single-use entry link (POST /api/admin/v1/internal-surfaces/db-studio/session) and opens it in a new tab. The link sets an HttpOnly session cookie on the console's own hostname, so it has to be opened by the browser rather than fetched — the nginx gate in front of the console then re-checks the permission on every request. The tab is opened synchronously inside the click handler, before the request resolves, or the popup blocker kills it; `noopener` is unusable there since it makes window.open return null, so the opener is detached manually. Gated on auth:db-studio:access. That key is `auth:`-prefixed rather than `admin:` on purpose — PermissionSeeder syncs the admin role to every `admin:%` key, which would grant database-owner SQL access to every admin account. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
29
src/api/modules/internal-surfaces.ts
Normal file
29
src/api/modules/internal-surfaces.ts
Normal file
@ -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<ApiResponse<InternalSurfaceSession>>(
|
||||
`/api/admin/v1/internal-surfaces/${surface}/session`,
|
||||
)
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@ -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,
|
||||
|
||||
69
src/routes/_authed/db-studio.tsx
Normal file
69
src/routes/_authed/db-studio.tsx
Normal file
@ -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 (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="数据库管理台"
|
||||
description="在浏览器中查询与编辑生产数据库(Supabase Studio)"
|
||||
/>
|
||||
<Card className="max-w-lg">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">打开管理台</CardTitle>
|
||||
<CardDescription>
|
||||
将在新标签页中打开。入口链接一次性有效、60 秒内过期,会话约 30
|
||||
分钟后失效,重新打开即可。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
管理台以数据库属主身份执行 SQL,操作不可撤销,且不会经过应用层的校验与审计。
|
||||
请优先使用后台既有功能,仅在排查问题时使用本工具。
|
||||
</p>
|
||||
<Button onClick={open} disabled={busy}>
|
||||
{busy ? '正在打开…' : '打开数据库管理台'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user