feat(vmis): 租戶自動開通完整流程 + Admin Portal SSO + NC 行事曆訂閱
Backend: - schedule_tenant: NC 新容器自動 pgsql 安裝 (_nc_db_check 全新容器處理) - schedule_tenant: NC 初始化加入 Redis + APCu memcache 設定 (修正 OIDC invalid_state) - schedule_tenant: 新租戶 KC realm 自動設定 accessCodeLifespan=600s (修正 authentication_expired) - schedule_account: NC Mail 帳號自動設定 (nc_mail_result/nc_mail_done_at) - schedule_account: NC 台灣國定假日行事曆自動訂閱 (CalDAV MKCALENDAR) - nextcloud_client: 新增 subscribe_calendar() CalDAV 訂閱方法 - settings: 新增系統設定 API (site_title/version/timezone/SSO/Keycloak) - models/result: 新增 nc_mail_result, nc_mail_done_at 欄位 - alembic: 遷移 002(system_settings) 003(keycloak_admin) 004(nc_mail_result) Frontend (Admin Portal): - 新增完整管理後台 (index/tenants/accounts/servers/schedules/logs/settings/system-status) - api.js: Keycloak JS Adapter SSO 整合 (PKCE/S256, fallback KC JS 來源, 自動 token 更新) - index.html: Promise.allSettled 取代 Promise.all,防止單一 API 失敗影響整頁 - 所有頁面加入 try/catch + toast 錯誤處理 - 新增品牌 LOGO 與 favicon Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,20 @@ class NextcloudClient:
|
||||
logger.error(f"NC create_user({username}) failed: {e}")
|
||||
return False
|
||||
|
||||
def set_user_quota(self, username: str, quota_gb: int) -> bool:
|
||||
try:
|
||||
resp = httpx.put(
|
||||
f"{self._base}/ocs/v1.php/cloud/users/{username}",
|
||||
auth=self._auth,
|
||||
headers=OCS_HEADERS,
|
||||
data={"key": "quota", "value": f"{quota_gb}GB"},
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
return resp.status_code == 200
|
||||
except Exception as e:
|
||||
logger.error(f"NC set_user_quota({username}) failed: {e}")
|
||||
return False
|
||||
|
||||
def get_user_quota_used_gb(self, username: str) -> Optional[float]:
|
||||
try:
|
||||
resp = httpx.get(
|
||||
@@ -57,11 +71,57 @@ class NextcloudClient:
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return None
|
||||
used_bytes = resp.json().get("ocs", {}).get("data", {}).get("quota", {}).get("used", 0)
|
||||
quota = resp.json().get("ocs", {}).get("data", {}).get("quota", {})
|
||||
if not isinstance(quota, dict):
|
||||
return 0.0
|
||||
used_bytes = quota.get("used", 0) or 0
|
||||
return round(used_bytes / 1073741824, 4)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def subscribe_calendar(
|
||||
self,
|
||||
username: str,
|
||||
cal_slug: str,
|
||||
display_name: str,
|
||||
ics_url: str,
|
||||
color: str = "#e9322d",
|
||||
) -> bool:
|
||||
"""
|
||||
為使用者建立訂閱行事曆(CalDAV MKCALENDAR with calendarserver source)。
|
||||
已存在(405)視為成功;201=新建成功。
|
||||
"""
|
||||
try:
|
||||
body = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<x1:mkcalendar xmlns:x1="urn:ietf:params:xml:ns:caldav">\n'
|
||||
' <x0:set xmlns:x0="DAV:">\n'
|
||||
' <x0:prop>\n'
|
||||
f' <x0:displayname>{display_name}</x0:displayname>\n'
|
||||
f' <x2:calendar-color xmlns:x2="http://apple.com/ns/ical/">{color}</x2:calendar-color>\n'
|
||||
' <x3:source xmlns:x3="http://calendarserver.org/ns/">\n'
|
||||
f' <x0:href>{ics_url}</x0:href>\n'
|
||||
' </x3:source>\n'
|
||||
' </x0:prop>\n'
|
||||
' </x0:set>\n'
|
||||
'</x1:mkcalendar>'
|
||||
)
|
||||
url = f"{self._base}/remote.php/dav/calendars/{username}/{cal_slug}/"
|
||||
resp = httpx.request(
|
||||
"MKCALENDAR",
|
||||
url,
|
||||
auth=self._auth,
|
||||
content=body.encode("utf-8"),
|
||||
headers={"Content-Type": "application/xml; charset=utf-8"},
|
||||
timeout=TIMEOUT,
|
||||
verify=False,
|
||||
)
|
||||
# 201=建立成功, 405=已存在(Method Not Allowed on existing resource)
|
||||
return resp.status_code in (201, 405)
|
||||
except Exception as e:
|
||||
logger.error(f"NC subscribe_calendar({username}, {cal_slug}) failed: {e}")
|
||||
return False
|
||||
|
||||
def get_total_quota_used_gb(self) -> Optional[float]:
|
||||
"""Sum all users' quota usage"""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user