feat(licensing): add WebUI license import modal, remove lic step from setup, default unlicensed to 0 seats
This commit is contained in:
@@ -3,7 +3,6 @@ set -euo pipefail
|
|||||||
|
|
||||||
INSTALL_DIR="/opt/logar-server"
|
INSTALL_DIR="/opt/logar-server"
|
||||||
CONFIG_DIR="/etc/logar"
|
CONFIG_DIR="/etc/logar"
|
||||||
LIC_FILE=""
|
|
||||||
|
|
||||||
# Parse command line options
|
# Parse command line options
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
@@ -12,10 +11,6 @@ while [[ $# -gt 0 ]]; do
|
|||||||
CONFIG_DIR="$2"
|
CONFIG_DIR="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--lic|--license|--import-lic)
|
|
||||||
LIC_FILE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
@@ -27,16 +22,11 @@ echo " LOGAR Server Enterprise Linux Installer"
|
|||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
# Interactive configuration if running in a terminal without CLI arguments
|
# Interactive configuration if running in a terminal without CLI arguments
|
||||||
if [ -t 0 ] && [ -z "${LIC_FILE}" ]; then
|
if [ -t 0 ]; then
|
||||||
read -r -p "Enter Data and Configuration Directory [${CONFIG_DIR}]: " USER_CFG
|
read -r -p "Enter Data and Configuration Directory [${CONFIG_DIR}]: " USER_CFG
|
||||||
if [ -n "${USER_CFG}" ]; then
|
if [ -n "${USER_CFG}" ]; then
|
||||||
CONFIG_DIR="${USER_CFG}"
|
CONFIG_DIR="${USER_CFG}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
read -r -p "Enter path to .lic license file (press Enter to skip): " USER_LIC
|
|
||||||
if [ -n "${USER_LIC}" ]; then
|
|
||||||
LIC_FILE="${USER_LIC}"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}" "/var/log/logar"
|
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}" "/var/log/logar"
|
||||||
@@ -55,12 +45,6 @@ if [ -f "${INSTALL_DIR}/Server" ]; then
|
|||||||
chmod +x "${INSTALL_DIR}/Server"
|
chmod +x "${INSTALL_DIR}/Server"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If license file provided, import it during setup
|
|
||||||
if [ -n "${LIC_FILE}" ] && [ -f "${LIC_FILE}" ]; then
|
|
||||||
echo "[*] Importing license file: ${LIC_FILE}..."
|
|
||||||
"${INSTALL_DIR}/Server" --config "${CONFIG_DIR}/server_config.json" --import-lic "${LIC_FILE}" || echo "[!] License import returned warning. Continuing."
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat <<EOF > /etc/systemd/system/logar-server.service
|
cat <<EOF > /etc/systemd/system/logar-server.service
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=LOGAR Hub and Aggregator Engine
|
Description=LOGAR Hub and Aggregator Engine
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ Type: filesandordirs; Name: "{commonappdata}\LOGAR-Server"
|
|||||||
[Code]
|
[Code]
|
||||||
var
|
var
|
||||||
DataDirPage: TInputDirWizardPage;
|
DataDirPage: TInputDirWizardPage;
|
||||||
LicFilePage: TInputFileWizardPage;
|
|
||||||
|
|
||||||
procedure InitializeWizard;
|
procedure InitializeWizard;
|
||||||
begin
|
begin
|
||||||
@@ -66,15 +65,6 @@ begin
|
|||||||
);
|
);
|
||||||
DataDirPage.Add('Data Directory:');
|
DataDirPage.Add('Data Directory:');
|
||||||
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR-Server');
|
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR-Server');
|
||||||
|
|
||||||
LicFilePage := CreateInputFilePage(
|
|
||||||
DataDirPage.ID,
|
|
||||||
'Enterprise License Activation (Optional)',
|
|
||||||
'Do you have an activated .lic license file to import during installation?',
|
|
||||||
'Select your signed .lic file to activate the server immediately.'#13#10#13#10 +
|
|
||||||
'You can also leave this blank to initialize with a default evaluation license and activate later via the web dashboard.'
|
|
||||||
);
|
|
||||||
LicFilePage.Add('License File (.lic):', 'License Files (*.lic)|*.lic|All Files (*.*)|*.*', '.lic');
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function GetDataDir(Param: String): String;
|
function GetDataDir(Param: String): String;
|
||||||
@@ -84,32 +74,11 @@ begin
|
|||||||
Result := ExpandConstant('{commonappdata}\LOGAR-Server');
|
Result := ExpandConstant('{commonappdata}\LOGAR-Server');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function GetLicFile(Param: String): String;
|
|
||||||
begin
|
|
||||||
Result := LicFilePage.Values[0];
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure CurStepChanged(CurStep: TSetupStep);
|
procedure CurStepChanged(CurStep: TSetupStep);
|
||||||
var
|
|
||||||
LicPath: String;
|
|
||||||
ResultCode: Integer;
|
|
||||||
begin
|
begin
|
||||||
if CurStep = ssPostInstall then
|
if CurStep = ssPostInstall then
|
||||||
begin
|
begin
|
||||||
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', GetDataDir(''));
|
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Server', 'DataDir', GetDataDir(''));
|
||||||
|
|
||||||
LicPath := GetLicFile('');
|
|
||||||
if (LicPath <> '') and FileExists(LicPath) then
|
|
||||||
begin
|
|
||||||
Exec(
|
|
||||||
ExpandConstant('{app}\Server.exe'),
|
|
||||||
'--config "' + GetDataDir('') + '\server_config.json" --import-lic "' + LicPath + '"',
|
|
||||||
ExpandConstant('{app}'),
|
|
||||||
SW_HIDE,
|
|
||||||
ewWaitUntilTerminated,
|
|
||||||
ResultCode
|
|
||||||
);
|
|
||||||
end;
|
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ VIAddVersionKey "FileVersion" "0.3.1.0"
|
|||||||
VIAddVersionKey "ProductVersion" "0.3.1.0"
|
VIAddVersionKey "ProductVersion" "0.3.1.0"
|
||||||
|
|
||||||
Var DATADIR
|
Var DATADIR
|
||||||
Var LICFILE
|
|
||||||
Var LicDialog
|
|
||||||
Var LicFileInput
|
|
||||||
Var LicBrowseBtn
|
|
||||||
|
|
||||||
# --- UI Pages ---
|
# --- UI Pages ---
|
||||||
!define MUI_ABORTWARNING
|
!define MUI_ABORTWARNING
|
||||||
@@ -44,9 +40,6 @@ Var LicBrowseBtn
|
|||||||
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
||||||
!insertmacro MUI_PAGE_DIRECTORY
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
# Custom Page: License File Selection
|
|
||||||
Page custom LicensePageCreate LicensePageLeave
|
|
||||||
|
|
||||||
# Instfiles Page
|
# Instfiles Page
|
||||||
!insertmacro MUI_PAGE_INSTFILES
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
|
||||||
@@ -64,44 +57,8 @@ Page custom LicensePageCreate LicensePageLeave
|
|||||||
Function .onInit
|
Function .onInit
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
StrCpy $DATADIR "$APPDATA\LOGAR-Server"
|
StrCpy $DATADIR "$APPDATA\LOGAR-Server"
|
||||||
StrCpy $LICFILE ""
|
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
Function LicensePageCreate
|
|
||||||
nsDialogs::Create 1018
|
|
||||||
Pop $LicDialog
|
|
||||||
${If} $LicDialog == error
|
|
||||||
Abort
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
!insertmacro MUI_HEADER_TEXT "Enterprise License Activation (Optional)" "Choose a .lic license file to activate your server during installation."
|
|
||||||
|
|
||||||
${NSD_CreateLabel} 0 0 100% 30u "If you have received an activated .lic license file, select it below to activate the server immediately.$\r$\n$\r$\nLeave this field blank to use the default evaluation license."
|
|
||||||
Pop $0
|
|
||||||
|
|
||||||
${NSD_CreateText} 0 38u 75% 12u "$LICFILE"
|
|
||||||
Pop $LicFileInput
|
|
||||||
|
|
||||||
${NSD_CreateButton} 78% 37u 22% 14u "Browse..."
|
|
||||||
Pop $LicBrowseBtn
|
|
||||||
${NSD_OnClick} $LicBrowseBtn OnBrowseLic
|
|
||||||
|
|
||||||
nsDialogs::Show
|
|
||||||
Function
|
|
||||||
|
|
||||||
Function OnBrowseLic
|
|
||||||
nsDialogs::SelectFileDialog open "" "LOGAR License Files (*.lic)|*.lic|All Files (*.*)|*.*"
|
|
||||||
Pop $0
|
|
||||||
${If} $0 != ""
|
|
||||||
StrCpy $LICFILE $0
|
|
||||||
${NSD_SetText} $LicFileInput $LICFILE
|
|
||||||
${EndIf}
|
|
||||||
Function
|
|
||||||
|
|
||||||
Function LicensePageLeave
|
|
||||||
${NSD_GetText} $LicFileInput $LICFILE
|
|
||||||
Function
|
|
||||||
|
|
||||||
Section "Install"
|
Section "Install"
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
SetOutPath "$INSTDIR"
|
SetOutPath "$INSTDIR"
|
||||||
@@ -112,12 +69,6 @@ Section "Install"
|
|||||||
CreateDirectory "$DATADIR"
|
CreateDirectory "$DATADIR"
|
||||||
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
|
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
|
||||||
|
|
||||||
# If license file was selected, import before starting service
|
|
||||||
${If} $LICFILE != ""
|
|
||||||
${AndIf} ${FileExists} "$LICFILE"
|
|
||||||
ExecWait '"$INSTDIR\Server.exe" --config "$DATADIR\server_config.json" --import-lic "$LICFILE"'
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
# Install and configure service via NSSM
|
# Install and configure service via NSSM
|
||||||
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Server "$INSTDIR\Server.exe"'
|
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Server "$INSTDIR\Server.exe"'
|
||||||
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppDirectory "$INSTDIR"'
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Server AppDirectory "$INSTDIR"'
|
||||||
|
|||||||
+148
-22
@@ -470,7 +470,7 @@ def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]:
|
|||||||
if "enrollment_secret" not in config:
|
if "enrollment_secret" not in config:
|
||||||
config["enrollment_secret"] = secrets.token_hex(24)
|
config["enrollment_secret"] = secrets.token_hex(24)
|
||||||
if "max_seats" not in config:
|
if "max_seats" not in config:
|
||||||
config["max_seats"] = 10
|
config["max_seats"] = 0
|
||||||
if "cert_dir" not in config:
|
if "cert_dir" not in config:
|
||||||
config["cert_dir"] = "certs"
|
config["cert_dir"] = "certs"
|
||||||
if "tls_enabled" not in config:
|
if "tls_enabled" not in config:
|
||||||
@@ -517,7 +517,8 @@ def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]:
|
|||||||
"auth_token": auth_token,
|
"auth_token": auth_token,
|
||||||
"enrollment_secret": enrollment_secret,
|
"enrollment_secret": enrollment_secret,
|
||||||
"session_secret": session_sec,
|
"session_secret": session_sec,
|
||||||
"max_seats": 10,
|
"max_seats": 0,
|
||||||
|
"license_status": "unlicensed",
|
||||||
"license_server_url": "https://logar.eibl.tech",
|
"license_server_url": "https://logar.eibl.tech",
|
||||||
"cert_dir": cert_dir,
|
"cert_dir": cert_dir,
|
||||||
"tls_enabled": True,
|
"tls_enabled": True,
|
||||||
@@ -646,7 +647,7 @@ def format_relative_time(dt_str: Optional[str]) -> str:
|
|||||||
return dt_str[:16] if dt_str else "N/A"
|
return dt_str[:16] if dt_str else "N/A"
|
||||||
|
|
||||||
|
|
||||||
def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: int = 10) -> str:
|
def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: int = 0) -> str:
|
||||||
"""Initializes the SQLite schema for multi-run temporal tracking, client tracking, users, and license quota."""
|
"""Initializes the SQLite schema for multi-run temporal tracking, client tracking, users, and license quota."""
|
||||||
def _execute_schema(target_path: str):
|
def _execute_schema(target_path: str):
|
||||||
target_dir = os.path.dirname(os.path.abspath(target_path))
|
target_dir = os.path.dirname(os.path.abspath(target_path))
|
||||||
@@ -692,13 +693,13 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in
|
|||||||
conn.execute("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS license_config (
|
CREATE TABLE IF NOT EXISTS license_config (
|
||||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
max_seats INTEGER NOT NULL DEFAULT 10,
|
max_seats INTEGER NOT NULL DEFAULT 0,
|
||||||
enrollment_secret TEXT NOT NULL,
|
enrollment_secret TEXT NOT NULL,
|
||||||
license_id TEXT,
|
license_id TEXT,
|
||||||
license_server_url TEXT,
|
license_server_url TEXT,
|
||||||
issued_at TEXT,
|
issued_at TEXT,
|
||||||
expires_at TEXT,
|
expires_at TEXT,
|
||||||
license_status TEXT DEFAULT 'active'
|
license_status TEXT DEFAULT 'unlicensed'
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -708,7 +709,7 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in
|
|||||||
("license_server_url", "TEXT"),
|
("license_server_url", "TEXT"),
|
||||||
("issued_at", "TEXT"),
|
("issued_at", "TEXT"),
|
||||||
("expires_at", "TEXT"),
|
("expires_at", "TEXT"),
|
||||||
("license_status", "TEXT DEFAULT 'active'")
|
("license_status", "TEXT DEFAULT 'unlicensed'")
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
conn.execute(f"ALTER TABLE license_config ADD COLUMN {col} {col_def}")
|
conn.execute(f"ALTER TABLE license_config ADD COLUMN {col} {col_def}")
|
||||||
@@ -769,8 +770,8 @@ def init_db(db_path: str, enrollment_secret: Optional[str] = None, max_seats: in
|
|||||||
|
|
||||||
if enrollment_secret:
|
if enrollment_secret:
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
INSERT OR IGNORE INTO license_config (id, max_seats, enrollment_secret)
|
INSERT OR IGNORE INTO license_config (id, max_seats, enrollment_secret, license_status)
|
||||||
VALUES (1, ?, ?)
|
VALUES (1, ?, ?, 'unlicensed')
|
||||||
""", (max_seats, enrollment_secret))
|
""", (max_seats, enrollment_secret))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -1317,13 +1318,18 @@ def enroll_client(req: ClientEnrollRequest, request: Request = None):
|
|||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
# 1. Validate enrollment secret against license_config
|
# 1. Validate enrollment secret against license_config
|
||||||
c.execute("SELECT enrollment_secret, max_seats FROM license_config WHERE id = 1")
|
c.execute("SELECT enrollment_secret, max_seats, license_status FROM license_config WHERE id = 1")
|
||||||
row = c.fetchone()
|
row = c.fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
conn.close()
|
conn.close()
|
||||||
raise HTTPException(status_code=500, detail="License configuration not initialized")
|
raise HTTPException(status_code=500, detail="License configuration not initialized")
|
||||||
|
|
||||||
expected_secret, max_seats = row
|
expected_secret, max_seats = row[0], row[1]
|
||||||
|
license_status = row[2] if len(row) > 2 and row[2] else "unlicensed"
|
||||||
|
if max_seats <= 0 or license_status != "active":
|
||||||
|
conn.close()
|
||||||
|
raise HTTPException(status_code=403, detail="Server is unlicensed. Import an activated .lic file via the WebUI before enrolling edge clients.")
|
||||||
|
|
||||||
if not secrets.compare_digest(str(req.enrollment_secret), str(expected_secret)):
|
if not secrets.compare_digest(str(req.enrollment_secret), str(expected_secret)):
|
||||||
conn.close()
|
conn.close()
|
||||||
record_failed_ip_attempt(client_ip, _ENROLL_FAILURES)
|
record_failed_ip_attempt(client_ip, _ENROLL_FAILURES)
|
||||||
@@ -1647,14 +1653,18 @@ def get_license_summary() -> Dict[str, Any]:
|
|||||||
ca_path = os.path.join(cert_dir, "ca.crt")
|
ca_path = os.path.join(cert_dir, "ca.crt")
|
||||||
db_path = config.get("db_path", DEFAULT_DB_FILE)
|
db_path = config.get("db_path", DEFAULT_DB_FILE)
|
||||||
|
|
||||||
max_seats = config.get("max_seats", 10)
|
max_seats = config.get("max_seats", 0)
|
||||||
|
license_status_db = "unlicensed"
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(db_path)
|
conn = sqlite3.connect(db_path)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("SELECT max_seats FROM license_config WHERE id = 1")
|
c.execute("SELECT max_seats, license_status FROM license_config WHERE id = 1")
|
||||||
row = c.fetchone()
|
row = c.fetchone()
|
||||||
if row and row[0]:
|
if row:
|
||||||
max_seats = row[0]
|
if row[0] is not None:
|
||||||
|
max_seats = row[0]
|
||||||
|
if len(row) > 1 and row[1]:
|
||||||
|
license_status_db = row[1]
|
||||||
c.execute("SELECT COUNT(*) FROM clients WHERE status = 'active'")
|
c.execute("SELECT COUNT(*) FROM clients WHERE status = 'active'")
|
||||||
active_seats = c.fetchone()[0]
|
active_seats = c.fetchone()[0]
|
||||||
c.execute("SELECT COUNT(*) FROM clients WHERE status = 'revoked'")
|
c.execute("SELECT COUNT(*) FROM clients WHERE status = 'revoked'")
|
||||||
@@ -1679,13 +1689,15 @@ def get_license_summary() -> Dict[str, Any]:
|
|||||||
else:
|
else:
|
||||||
info = {"valid": False, "is_self_signed": False, "is_expired": False, "days_remaining": 0}
|
info = {"valid": False, "is_self_signed": False, "is_expired": False, "days_remaining": 0}
|
||||||
|
|
||||||
license_id = info.get("license_id") or "UNLICENSED-LOCAL"
|
license_id = info.get("license_id") or "UNLICENSED"
|
||||||
issuer_url = info.get("issuer_url") or config.get("license_server_url", "https://logar.eibl.tech")
|
issuer_url = info.get("issuer_url") or config.get("license_server_url", "https://logar.eibl.tech")
|
||||||
status_label = "ACTIVE"
|
|
||||||
if info.get("is_expired"):
|
if info.get("is_expired"):
|
||||||
status_label = "EXPIRED"
|
status_label = "EXPIRED"
|
||||||
elif not info.get("seats") and info.get("is_self_signed"):
|
elif max_seats <= 0 or license_status_db != "active" or info.get("is_self_signed") or not info.get("seats"):
|
||||||
status_label = "SELF-SIGNED"
|
status_label = "UNLICENSED"
|
||||||
|
else:
|
||||||
|
status_label = "ACTIVE"
|
||||||
|
|
||||||
pct = int((active_seats / max_seats * 100)) if max_seats > 0 else 0
|
pct = int((active_seats / max_seats * 100)) if max_seats > 0 else 0
|
||||||
return {
|
return {
|
||||||
@@ -1749,7 +1761,7 @@ async def import_signed_license(req: Request):
|
|||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status_code=400, detail="Invalid JSON body")
|
raise HTTPException(status_code=400, detail="Invalid JSON body")
|
||||||
|
|
||||||
raw_input = body.get("ca_cert") or body.get("lic") or body.get("raw")
|
raw_input = body.get("ca_cert") or body.get("lic") or body.get("raw") or body.get("license") or body.get("license_file")
|
||||||
if not raw_input:
|
if not raw_input:
|
||||||
raise HTTPException(status_code=400, detail="Missing 'lic' or 'ca_cert' payload in request")
|
raise HTTPException(status_code=400, detail="Missing 'lic' or 'ca_cert' payload in request")
|
||||||
|
|
||||||
@@ -1762,13 +1774,18 @@ async def import_signed_license(req: Request):
|
|||||||
cert_dir = config.get("cert_dir", "certs")
|
cert_dir = config.get("cert_dir", "certs")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = enrollment.import_license_package(raw_text, cert_dir=cert_dir)
|
res = enrollment.import_license_package(
|
||||||
|
raw_text,
|
||||||
|
cert_dir=cert_dir,
|
||||||
|
server_fingerprint=config.get("server_fingerprint", ""),
|
||||||
|
hostname=config.get("server_name", socket.gethostname())
|
||||||
|
)
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
raise HTTPException(status_code=400, detail=str(ve))
|
raise HTTPException(status_code=400, detail=str(ve))
|
||||||
|
|
||||||
info = res["license_info"]
|
info = res["license_info"]
|
||||||
ca_cert_pem = res["ca_cert_pem"]
|
ca_cert_pem = res["ca_cert_pem"]
|
||||||
new_seats = info.get("seats") or config.get("max_seats", 10)
|
new_seats = info.get("seats") or config.get("max_seats", 0)
|
||||||
config["max_seats"] = new_seats
|
config["max_seats"] = new_seats
|
||||||
db_path = config.get("db_path", DEFAULT_DB_FILE)
|
db_path = config.get("db_path", DEFAULT_DB_FILE)
|
||||||
conn = sqlite3.connect(db_path)
|
conn = sqlite3.connect(db_path)
|
||||||
@@ -2013,6 +2030,24 @@ def serve_webui(request: Request = None):
|
|||||||
bar_color = "#10b981" if pct < 70 else ("#f59e0b" if pct < 90 else "#ef4444")
|
bar_color = "#10b981" if pct < 70 else ("#f59e0b" if pct < 90 else "#ef4444")
|
||||||
badge_status_cls = "badge-active" if summary["status"] == "ACTIVE" else ("badge-warning" if summary["status"] == "SELF-SIGNED" else "badge-revoked")
|
badge_status_cls = "badge-active" if summary["status"] == "ACTIVE" else ("badge-warning" if summary["status"] == "SELF-SIGNED" else "badge-revoked")
|
||||||
|
|
||||||
|
unlicensed_banner_html = ""
|
||||||
|
if summary["status"] == "UNLICENSED" or summary["max_seats"] == 0:
|
||||||
|
unlicensed_banner_html = f"""
|
||||||
|
<div style="background: rgba(239, 68, 68, 0.12); border: 1px solid rgba(239, 68, 68, 0.35); border-radius: 10px; padding: 16px 20px; margin-bottom: 24px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 14px;">
|
||||||
|
<div style="font-size: 28px;">⚠️</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 700; color: #f87171; font-size: 15px; margin-bottom: 2px;">LOGAR Server is Unlicensed (0 Active Seats)</div>
|
||||||
|
<div style="font-size: 13px; color: #cbd5e1;">Forwarder enrollment and log ingestion are restricted. Import an activated <code>.lic</code> package to enable edge client connections and seat provisioning.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<button onclick="downloadLicRequest()" class="btn btn-secondary" style="font-size: 12px; padding: 7px 14px;">📤 Export License Request</button>
|
||||||
|
<button onclick="openImportLicModal()" class="btn btn-accent" style="font-size: 12px; padding: 7px 14px;">📥 Import License (.lic)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
detected_host = (request.headers.get("host", "").split(":")[0] if request and request.headers else "") or config.get("tcp_host", "127.0.0.1")
|
detected_host = (request.headers.get("host", "").split(":")[0] if request and request.headers else "") or config.get("tcp_host", "127.0.0.1")
|
||||||
if detected_host in ("0.0.0.0", "localhost", ""):
|
if detected_host in ("0.0.0.0", "localhost", ""):
|
||||||
detected_host = socket.gethostname()
|
detected_host = socket.gethostname()
|
||||||
@@ -2232,6 +2267,8 @@ def serve_webui(request: Request = None):
|
|||||||
|
|
||||||
<div id="statusAlert" class="alert"></div>
|
<div id="statusAlert" class="alert"></div>
|
||||||
|
|
||||||
|
{unlicensed_banner_html}
|
||||||
|
|
||||||
<!-- Navigation Tabs -->
|
<!-- Navigation Tabs -->
|
||||||
<div class="tabs-nav">
|
<div class="tabs-nav">
|
||||||
<button class="tab-btn active" id="tabBtn-fleet" onclick="switchTab('fleet')">💻 Fleet Forwarders ({len(clients)})</button>
|
<button class="tab-btn active" id="tabBtn-fleet" onclick="switchTab('fleet')">💻 Fleet Forwarders ({len(clients)})</button>
|
||||||
@@ -2383,7 +2420,10 @@ def serve_webui(request: Request = None):
|
|||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div style="font-weight: 600; font-size: 13px; color: var(--text-muted);">PKI & Seat Synchronization:</div>
|
<div style="font-weight: 600; font-size: 13px; color: var(--text-muted);">PKI & Seat Synchronization:</div>
|
||||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||||
<button class="btn btn-accent" onclick="triggerMtlsRenewal()">🔄 Sync & Renew via mTLS</button>
|
<button class="btn btn-accent" onclick="openImportLicModal()">📥 Import License (.lic)</button>
|
||||||
|
<button class="btn btn-secondary" onclick="downloadLicRequest()">📤 Export License Request (.lic)</button>
|
||||||
|
<button class="btn btn-outline" onclick="copyCaCsr()">📋 Copy CA CSR</button>
|
||||||
|
<button class="btn btn-secondary" onclick="triggerMtlsRenewal()">🔄 Sync & Renew via mTLS</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2459,6 +2499,28 @@ def serve_webui(request: Request = None):
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Import License Modal -->
|
||||||
|
<div id="importLicModal" class="modal">
|
||||||
|
<div class="modal-content" style="max-width: 600px;">
|
||||||
|
<div class="modal-title">📥 Import Enterprise License (.lic)</div>
|
||||||
|
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 16px;">
|
||||||
|
Select a signed <code>.lic</code> file from your computer or paste the raw license JSON / Sub-CA PEM certificate below to activate server seats.
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Upload .lic File</label>
|
||||||
|
<input type="file" id="licFileInput" class="form-control" accept=".lic,.json,.crt,.pem" onchange="handleLicFile(event)">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Or Paste License JSON / PEM</label>
|
||||||
|
<textarea id="licRawText" class="form-control" rows="8" placeholder="Paste .lic JSON content or PEM certificate here..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button onclick="closeModal('importLicModal')" class="btn btn-outline">Cancel</button>
|
||||||
|
<button onclick="submitImportLic()" class="btn btn-accent">Activate License</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Password Modal -->
|
<!-- Password Modal -->
|
||||||
<div id="passwordModal" class="modal">
|
<div id="passwordModal" class="modal">
|
||||||
<div class="modal-content" style="max-width: 440px;">
|
<div class="modal-content" style="max-width: 440px;">
|
||||||
@@ -2511,8 +2573,72 @@ def serve_webui(request: Request = None):
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
function openExportProfileModal() {{ openModal('exportProfileModal'); }}
|
function openExportProfileModal() {{ openModal('exportProfileModal'); }}
|
||||||
|
function openImportLicModal() {{ openModal('importLicModal'); }}
|
||||||
function openPasswordModal() {{ openModal('passwordModal'); }}
|
function openPasswordModal() {{ openModal('passwordModal'); }}
|
||||||
|
|
||||||
|
function downloadLicRequest() {{
|
||||||
|
window.location.href = '/api/license/export-lic';
|
||||||
|
}}
|
||||||
|
|
||||||
|
async function copyCaCsr() {{
|
||||||
|
try {{
|
||||||
|
const res = await fetch('/api/license/csr');
|
||||||
|
if (res.ok) {{
|
||||||
|
const data = await res.json();
|
||||||
|
navigator.clipboard.writeText(data.csr);
|
||||||
|
showAlert('CA CSR copied to clipboard!');
|
||||||
|
}} else {{
|
||||||
|
alert('Failed to fetch CA CSR');
|
||||||
|
}}
|
||||||
|
}} catch (err) {{
|
||||||
|
alert('Error fetching CA CSR: ' + err.message);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
function handleLicFile(event) {{
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {{
|
||||||
|
document.getElementById('licRawText').value = e.target.result;
|
||||||
|
}};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}}
|
||||||
|
|
||||||
|
async function submitImportLic() {{
|
||||||
|
const raw = document.getElementById('licRawText').value.trim();
|
||||||
|
if (!raw) return alert('Please select a .lic file or paste the license payload');
|
||||||
|
showAlert('Importing and activating license...');
|
||||||
|
try {{
|
||||||
|
let payload;
|
||||||
|
if (raw.startsWith('{{')) {{
|
||||||
|
try {{
|
||||||
|
payload = JSON.parse(raw);
|
||||||
|
}} catch (e) {{
|
||||||
|
payload = {{ "lic": raw }};
|
||||||
|
}}
|
||||||
|
}} else {{
|
||||||
|
payload = {{ "lic": raw }};
|
||||||
|
}}
|
||||||
|
|
||||||
|
const res = await fetch('/api/license/import', {{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {{ 'Content-Type': 'application/json' }},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {{
|
||||||
|
showAlert('License Activated! ' + (data.message || ''));
|
||||||
|
closeModal('importLicModal');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
}} else {{
|
||||||
|
alert('License Import Failed: ' + (data.detail || 'Validation error'));
|
||||||
|
}}
|
||||||
|
}} catch (err) {{
|
||||||
|
alert('Import request failed: ' + err.message);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
function downloadClientProfile() {{
|
function downloadClientProfile() {{
|
||||||
const host = document.getElementById('exportHost').value.trim();
|
const host = document.getElementById('exportHost').value.trim();
|
||||||
const port = document.getElementById('exportPort').value.trim();
|
const port = document.getElementById('exportPort').value.trim();
|
||||||
|
|||||||
Reference in New Issue
Block a user