feat: isolate households and add explicit ownership succession
This commit is contained in:
@@ -31,6 +31,7 @@ import onvif
|
||||
import playback
|
||||
import identity
|
||||
import genealogy
|
||||
import households
|
||||
import ptz
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
@@ -73,6 +74,7 @@ class Problem(Exception):
|
||||
|
||||
ACCOUNTS = identity.Accounts(sys.modules[__name__])
|
||||
FAMILY = genealogy.Family(sys.modules[__name__])
|
||||
HOUSEHOLDS = households.Households(sys.modules[__name__])
|
||||
|
||||
|
||||
def setting(key, default=None):
|
||||
@@ -144,9 +146,9 @@ SITE_FIELDS = ('name', 'province', 'city', 'district', 'street', 'community', 'o
|
||||
|
||||
def serialized_save(function):
|
||||
@functools.wraps(function)
|
||||
def save(body):
|
||||
def save(body, *args, **kwargs):
|
||||
with LOCK:
|
||||
return function(body)
|
||||
return function(body, *args, **kwargs)
|
||||
return save
|
||||
|
||||
|
||||
@@ -162,11 +164,12 @@ def object_key(table, body):
|
||||
|
||||
|
||||
@serialized_save
|
||||
def save_site(body):
|
||||
def save_site(body, actor=None):
|
||||
family = HOUSEHOLDS.prepare(actor, 'sites', body)
|
||||
key = object_key('sites', body)
|
||||
item = {k: clean_text(body.get(k, ''), 240 if k in ('address', 'note') else 100,
|
||||
required=k == 'name') for k in SITE_FIELDS}
|
||||
item.update(id=key, kind=body.get('kind', 'home'))
|
||||
item.update(id=key, kind=body.get('kind', 'home'), familyId=family)
|
||||
if item['kind'] not in ('home', 'company', 'other'):
|
||||
raise Problem('请选择家庭、公司或其他用途')
|
||||
save_object('sites', item)
|
||||
@@ -175,7 +178,8 @@ def save_site(body):
|
||||
|
||||
|
||||
@serialized_save
|
||||
def save_camera(body):
|
||||
def save_camera(body, actor=None):
|
||||
family = HOUSEHOLDS.prepare(actor, 'cameras', body, (('assets', 'assetId'), ('recorders', 'recorderId'), ('recorders', 'archiveRecorderId')))
|
||||
key = object_key('cameras', body)
|
||||
old = get_object('cameras', key) or {}
|
||||
item = {k: clean_text(body.get(k, ''), 120, required=k == 'name')
|
||||
@@ -201,7 +205,8 @@ def save_camera(body):
|
||||
raise Problem('关联的录像存储设备不存在')
|
||||
item['archiveRecorderId'] = archive_recorder
|
||||
item.update(id=key, siteId=site, host=host_address(recorder['host'] if recorder else body.get('host', '')),
|
||||
port=integer(recorder['port'] if recorder else body.get('port', 554), 1, 65535))
|
||||
port=integer(recorder['port'] if recorder else body.get('port', 554), 1, 65535), familyId=family)
|
||||
HOUSEHOLDS.endpoint(actor, item['host'])
|
||||
for k, default in [('mainPath', '/stream1'), ('subPath', '/stream2')]:
|
||||
path = clean_text(body.get(k, default), 500, required=True)
|
||||
if not path.startswith('/') or path.startswith('//') or '#' in path:
|
||||
@@ -224,14 +229,15 @@ def save_camera(body):
|
||||
|
||||
|
||||
@serialized_save
|
||||
def save_asset(body):
|
||||
def save_asset(body, actor=None):
|
||||
family = HOUSEHOLDS.prepare(actor, 'assets', body, (('sites', 'siteId'),))
|
||||
key = object_key('assets', body)
|
||||
site = body.get('siteId')
|
||||
if not get_object('sites', site):
|
||||
raise Problem('请选择所属空间')
|
||||
item = {k: clean_text(body.get(k, ''), 120, required=k == 'name')
|
||||
for k in ('name', 'brand', 'model', 'serial', 'point', 'note')}
|
||||
item.update(id=key, siteId=site, lensCount=integer(body.get('lensCount', 2), 1, 16))
|
||||
item.update(id=key, siteId=site, lensCount=integer(body.get('lensCount', 2), 1, 16), familyId=family)
|
||||
attached = [c for c in objects('cameras') if c.get('assetId') == key]
|
||||
if len(attached) > item['lensCount']:
|
||||
raise Problem('镜头数不能小于已经建立的通道数')
|
||||
@@ -245,7 +251,8 @@ def save_asset(body):
|
||||
|
||||
|
||||
@serialized_save
|
||||
def save_recorder(body):
|
||||
def save_recorder(body, actor=None):
|
||||
family = HOUSEHOLDS.prepare(actor, 'recorders', body, (('sites', 'siteId'),))
|
||||
key = object_key('recorders', body)
|
||||
old = get_object('recorders', key) or {}
|
||||
item = {k: clean_text(body.get(k, ''), 120, required=k == 'name')
|
||||
@@ -254,7 +261,8 @@ def save_recorder(body):
|
||||
if not get_object('sites', site):
|
||||
raise Problem('请选择录像机所属空间')
|
||||
item.update(id=key, siteId=site, host=host_address(body.get('host', '')),
|
||||
port=integer(body.get('port', 554), 1, 65535), driver=body.get('driver', 'generic-rtsp'))
|
||||
port=integer(body.get('port', 554), 1, 65535), driver=body.get('driver', 'generic-rtsp'), familyId=family)
|
||||
HOUSEHOLDS.endpoint(actor, item['host'])
|
||||
if item['driver'] not in ('generic-rtsp', 'hikvision-rtsp', 'tplink-rtsp', 'dahua-rtsp'):
|
||||
raise Problem('接入驱动不正确')
|
||||
password = body.get('password', '')
|
||||
@@ -514,6 +522,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.user = self.authenticated()
|
||||
if not self.user:
|
||||
raise Problem('请先登录', 401)
|
||||
self.user = HOUSEHOLDS.context(self.user, self.headers.get('X-Household-Id'))
|
||||
|
||||
def require_manager(self):
|
||||
if not HOUSEHOLDS.manager(self.user):
|
||||
raise Problem('此操作需要家庭管理员权限', 403)
|
||||
|
||||
def require_admin(self):
|
||||
if self.user['role'] != 'admin':
|
||||
@@ -598,7 +611,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.require_auth()
|
||||
if path not in ('/api/logout', '/api/account/password', '/api/family/restore',
|
||||
'/api/family/person', '/api/family/link', '/api/family/event', '/api/ptz', '/api/ptz/presets'):
|
||||
self.require_admin()
|
||||
self.require_manager()
|
||||
if path == '/api/logout':
|
||||
with LOCK:
|
||||
DB.execute('DELETE FROM sessions WHERE hash=?', (hashlib.sha256(self.token().encode()).hexdigest(),))
|
||||
@@ -622,7 +635,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
elif path == '/api/ptz/presets':
|
||||
c = ACCOUNTS.control(self.user, get_object('cameras', data.get('camera')))
|
||||
if data.get('action') != 'goto':
|
||||
self.require_admin()
|
||||
self.require_manager()
|
||||
try:
|
||||
result = ptz.CONTROLLER.preset_command(c, data)
|
||||
except ValueError as e:
|
||||
@@ -643,19 +656,26 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.answer(result)
|
||||
elif path == '/api/users':
|
||||
self.answer(ACCOUNTS.save(data, self.user))
|
||||
elif path == '/api/households':
|
||||
self.answer(HOUSEHOLDS.save(data, self.user))
|
||||
elif path == '/api/households/leadership':
|
||||
self.answer(HOUSEHOLDS.leadership(data, self.user))
|
||||
elif path in ('/api/family/person', '/api/family/link', '/api/family/event'):
|
||||
self.answer(FAMILY.save(path.rsplit('/', 1)[-1], data, self.user))
|
||||
elif path == '/api/sites':
|
||||
self.answer(save_site(data))
|
||||
self.answer(save_site(data, self.user))
|
||||
elif path == '/api/cameras':
|
||||
self.answer(save_camera(data))
|
||||
self.answer(save_camera(data, self.user))
|
||||
elif path == '/api/assets':
|
||||
self.answer(save_asset(data))
|
||||
self.answer(save_asset(data, self.user))
|
||||
elif path == '/api/recorders':
|
||||
self.answer(save_recorder(data))
|
||||
self.answer(save_recorder(data, self.user))
|
||||
elif path == '/api/onvif':
|
||||
old = get_object('cameras', data.get('id')) or {}
|
||||
if data.get('id'):
|
||||
HOUSEHOLDS.owns(self.user, old)
|
||||
host = host_address(data.get('host', ''))
|
||||
HOUSEHOLDS.endpoint(self.user, host)
|
||||
port = integer(data.get('port', 80), 1, 65535)
|
||||
username = clean_text(data.get('username', ''), 120, required=True)
|
||||
password = data.get('password') or old.get('password', '')
|
||||
@@ -667,6 +687,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
raise Problem('ONVIF 读取未成功,请检查账号、设备时间、服务端口及 ONVIF 是否启用', 502)
|
||||
self.answer(result)
|
||||
elif path == '/api/storage':
|
||||
self.require_admin()
|
||||
policy = {'retentionDays': integer(data.get('retentionDays'), 1, 365),
|
||||
'maxGB': integer(data.get('maxGB'), 5, 100000),
|
||||
'reserveGB': integer(data.get('reserveGB'), 2, 1000)}
|
||||
@@ -675,6 +696,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
audit('更新录像保留策略')
|
||||
self.answer({'ok': True})
|
||||
elif path == '/api/discover':
|
||||
self.require_admin()
|
||||
try:
|
||||
network = ipaddress.ip_network(data.get('network', ''), strict=False)
|
||||
if network.version != 4 or network.prefixlen < 24 or network.prefixlen > 30:
|
||||
@@ -777,6 +799,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
query = {k: v[0] for k, v in url.parse_qs(parsed.query).items()}
|
||||
if path == '/api/session':
|
||||
user = self.authenticated()
|
||||
user = HOUSEHOLDS.context(user, self.headers.get('X-Household-Id'))
|
||||
self.answer({'authenticated': bool(user), 'user': ACCOUNTS.public(user),
|
||||
'setupRequired': not bool(objects('users'))})
|
||||
return
|
||||
@@ -788,8 +811,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if path == '/api/state':
|
||||
self.answer(ACCOUNTS.state(self.user, status()))
|
||||
elif path == '/api/users':
|
||||
self.require_admin()
|
||||
self.answer({'users': [ACCOUNTS.public(u) for u in objects('users')]})
|
||||
self.require_manager()
|
||||
self.answer({'users': [ACCOUNTS.public(u) for u in HOUSEHOLDS.objects(self.user, 'users')
|
||||
if self.user['role'] == 'admin' or u['role'] != 'admin']})
|
||||
elif path == '/api/households':
|
||||
self.answer({'households': HOUSEHOLDS.list(self.user), 'activeId': HOUSEHOLDS.scope(self.user)})
|
||||
elif path == '/api/ptz/presets':
|
||||
c = ACCOUNTS.control(self.user, get_object('cameras', query.get('camera')))
|
||||
try:
|
||||
@@ -798,7 +824,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
raise Problem(str(e), 400)
|
||||
except Exception:
|
||||
raise Problem('未能读取预置位,设备可能未开放该接口;方向微调仍可单独使用', 502)
|
||||
self.answer(dict(result, canManage=self.user['role'] == 'admin'))
|
||||
self.answer(dict(result, canManage=HOUSEHOLDS.manager(self.user)))
|
||||
elif path == '/api/ptz':
|
||||
c = ACCOUNTS.control(self.user, get_object('cameras', query.get('camera')))
|
||||
try:
|
||||
@@ -811,7 +837,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
elif path == '/api/family':
|
||||
self.answer(FAMILY.snapshot(self.user))
|
||||
elif path == '/api/camera/settings':
|
||||
self.require_admin()
|
||||
self.require_manager()
|
||||
c = ACCOUNTS.camera(self.user, get_object('cameras', query.get('camera')))
|
||||
try:
|
||||
result, _ = camera_settings.CONTROLLER.read(c, query.get('quality', 'main'))
|
||||
@@ -873,7 +899,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
raise Problem('接口不存在', 404)
|
||||
else:
|
||||
files = {'/': 'index.html', '/app.js': 'app.js', '/live-player.js': 'live-player.js', '/style.css': 'style.css',
|
||||
'/family.js': 'family.js', '/kinship.js': 'kinship.js',
|
||||
'/family.js': 'family.js', '/kinship.js': 'kinship.js', '/households.js': 'households.js',
|
||||
'/family-graph.js': 'family-graph.js', '/family-map.js': 'family-map.js',
|
||||
'/family-map.css': 'family-map.css', '/chinese-theme.css': 'chinese-theme.css',
|
||||
'/family-events.js': 'family-events.js',
|
||||
@@ -911,6 +937,7 @@ def initialize():
|
||||
global MEDIA_AUTH
|
||||
FAMILY.initialize()
|
||||
ACCOUNTS.initialize()
|
||||
HOUSEHOLDS.initialize()
|
||||
if not setting('storage'):
|
||||
set_setting('storage', {'retentionDays': 7, 'maxGB': 40, 'reserveGB': 8})
|
||||
if not setting('mediaSecret'):
|
||||
|
||||
Reference in New Issue
Block a user