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,
|
||
|
|
}
|