601451f22b
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>
75 lines
3.2 KiB
Python
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,
|
|
}
|