Files
cms-SiteForge/app/console_v819.py
T
浅念 601451f22b upgrade: adopt V8.21.3 (V8.13→8.21.3: P5 SFTP/S3, P6 multilingual, editor overhaul, enterprise hardening)
Overlay ~150 upstream items onto our V8.12 base, preserving trade-pro
theme, registry tests, and design docs. Re-applied our patch set
(discovery/env_for/blocks.css/type-mapping/theme_color/frontend entries,
test_theme_pro_v3 mods) — all anchors survived upstream evolution.

New platform enhancement (fixes upgrade-introduced regression):
multilingual.site_locale now honors settings_json.locale, plus
PUT /api/i18n/sites/{id}/locale — standalone English sites get
<html lang='en'> under the V8.15 locale-rewrite pipeline.

compose: adopt V8.21.3 (image 8.21.3-dev.1, /api/ready probes, MySQL/S3
env) with project name pinned to siteforge-v8-10 (data volumes).

Tests: 356 passed / 11 failed — all 11 confirmed pre-existing in
pristine V8.21.3 on this host (10 upstream Windows env + our build test
now green after locale fix). Patch registry updated in plan doc.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 12:31:03 +08:00

75 lines
3.2 KiB
Python

"""Authorized, versioned navigation contract for the single SiteForge console.
The frontend is not an authorization authority: every business API keeps its
own server-side membership checks. This endpoint only returns allowed context.
"""
from fastapi import APIRouter, HTTPException, Request
from . import db, enterprise
from .version import VERSION
router = APIRouter(prefix='/api/console', tags=['Console'])
NAVIGATION = (
('dashboard', '工作台', '/siteforge/dashboard', None),
('sites', '网站管理', '/siteforge/factory', None),
('content', '内容管理', '/siteforge/content', 'viewer'),
('design', '网站设计', '/siteforge/editor', 'viewer'),
('migration', '数据与迁移', '/siteforge/migration', 'viewer'),
('release', '构建与发布', '/siteforge/releases', 'viewer'),
('system', '系统管理', '/siteforge/enterprise', 'owner'),
)
def available_sites(request: Request):
if request.state.is_superadmin:
return db.rows('SELECT id,name,domain FROM sites ORDER BY updated_at DESC,id')
return db.rows('''SELECT s.id,s.name,s.domain FROM sites s
JOIN team_memberships m ON m.site_id=s.id
JOIN team_users u ON u.id=m.user_id
WHERE m.user_id=? AND u.disabled=0
ORDER BY s.updated_at DESC,s.id''', (request.state.account_id,))
def capabilities(request: Request, site_id: str | None):
role = 'superadmin' if request.state.is_superadmin else 'none'
if site_id and not request.state.is_superadmin:
membership = db.one('SELECT role FROM team_memberships WHERE site_id=? AND user_id=?',
(site_id, request.state.account_id))
role = membership['role'] if membership else 'none'
level = enterprise.ACCESS.get(role, -1)
return dict(role=role, view=level>=0, edit=level>=enterprise.ACCESS['editor'],
review=level>=enterprise.ACCESS['reviewer'],
deploy=level>=enterprise.ACCESS['owner'],
manage_members=level>=enterprise.ACCESS['owner'],
create_site=bool(request.state.is_superadmin),
manage_users=bool(request.state.is_superadmin))
@router.get('/bootstrap')
def bootstrap(request: Request, site_id: str = ''):
"""Return user-scoped menus, never expose unassigned websites or credentials."""
sites = available_sites(request)
ids = {x['id'] for x in sites}
if site_id and site_id not in ids:
raise HTTPException(403, '没有访问该网站的权限')
selected = site_id or (sites[0]['id'] if sites else None)
rights = capabilities(request, selected)
sections = []
for key, title, path, required in NAVIGATION:
if required and not selected:
continue
if required and not rights['view']:
continue
if key == 'system' and not (rights['manage_members'] or rights['manage_users']):
continue
sections.append({'key': key, 'title': title, 'path': path, 'requires_site': bool(required)})
return {
'version': VERSION,
'user': request.state.actor,
'superadmin': bool(request.state.is_superadmin),
'sites': sites,
'selected_site_id': selected,
'capabilities': rights,
'navigation': sections,
}