user management fix

This commit is contained in:
2026-09-17 16:14:19 +05:00
parent 298124c30f
commit 77c00c4ed5
3 changed files with 327 additions and 391 deletions
+265
View File
@@ -0,0 +1,265 @@
import json
import socket
from pathlib import Path
SOCKET_PATH = Path("/run/parental-control.sock")
SOCKET_TIMEOUT = 10
class IPCError(Exception):
"""Base exception for IPC errors."""
class DatabaseError(IPCError):
"""Raised when a database operation fails."""
class AuthorizationError(IPCError):
"""Raised when an administrative operation is denied."""
class IPCClient:
@staticmethod
def call(method, **arguments):
request = {
"method": method,
"args": arguments,
}
payload = (
json.dumps(
request,
separators=(",", ":"),
).encode("utf-8")
+ b"\n"
)
data = bytearray()
try:
with socket.socket(
socket.AF_UNIX,
socket.SOCK_STREAM,
) as connection:
connection.settimeout(
SOCKET_TIMEOUT
)
connection.connect(
str(SOCKET_PATH)
)
connection.sendall(
payload
)
while True:
chunk = connection.recv(
65536
)
if not chunk:
break
data.extend(chunk)
if b"\n" in chunk:
break
except FileNotFoundError:
raise IPCError(
"Parental control service is not running."
)
except ConnectionRefusedError:
raise IPCError(
"Could not connect to the parental control service."
)
except TimeoutError:
raise IPCError(
"The parental control service did not respond."
)
except OSError as exc:
raise IPCError(
f"Could not connect to the parental control service: {exc}"
)
if not data:
raise IPCError(
"The parental control service returned no response."
)
line = bytes(data).split(
b"\n",
1,
)[0]
try:
response = json.loads(
line.decode("utf-8")
)
except (
UnicodeDecodeError,
json.JSONDecodeError,
):
raise IPCError(
"The parental control service returned an invalid response."
)
if response.get("ok"):
return response.get("result")
error = response.get(
"error",
"Unknown error",
)
if response.get("error_type") == "authorization":
raise AuthorizationError(error)
if response.get("error_type") == "database":
raise DatabaseError(error)
raise IPCError(error)
class Database:
"""
Compatibility interface for the GUI.
The GUI keeps using Database.* methods, but all operations
now go through the root parental-control daemon instead of
accessing SQLite directly.
"""
@staticmethod
def get_users():
return IPCClient.call(
"get_users"
)
@staticmethod
def get_user(user_id):
return IPCClient.call(
"get_user",
user_id=user_id,
)
@staticmethod
def get_remaining_time(user_id):
result = IPCClient.call(
"get_remaining_time",
user_id=user_id,
)
return (
result[0],
result[1],
)
@staticmethod
def get_allowances(user_id):
result = IPCClient.call(
"get_allowances",
user_id=user_id,
)
return {
int(key): value
for key, value in result.items()
}
@staticmethod
def get_access_windows(user_id):
result = IPCClient.call(
"get_access_windows",
user_id=user_id,
)
return {
int(key): [
tuple(window)
for window in windows
]
for key, windows in result.items()
}
@staticmethod
def create_user(
username,
enabled,
allowances,
access_windows,
):
return IPCClient.call(
"create_user",
username=username,
enabled=enabled,
allowances={
str(key): value
for key, value in allowances.items()
},
access_windows={
str(key): [
list(window)
for window in windows
]
for key, windows in access_windows.items()
},
)
@staticmethod
def update_user(
user_id,
enabled,
allowances,
access_windows,
):
return IPCClient.call(
"update_user",
user_id=user_id,
enabled=enabled,
allowances={
str(key): value
for key, value in allowances.items()
},
access_windows={
str(key): [
list(window)
for window in windows
]
for key, windows in access_windows.items()
},
)
@staticmethod
def delete_user(user_id):
return IPCClient.call(
"delete_user",
user_id=user_id,
)
@staticmethod
def set_enabled(
user_id,
enabled,
):
return IPCClient.call(
"set_enabled",
user_id=user_id,
enabled=enabled,
)
@staticmethod
def add_temporary_time(
user_id,
seconds,
):
return IPCClient.call(
"add_temporary_time",
user_id=user_id,
seconds=seconds,
)
+2 -386
View File
@@ -1,6 +1,5 @@
import sys
import pwd
import sqlite3
from PySide6.QtCore import Qt, QTime
from PySide6.QtWidgets import (
@@ -26,389 +25,7 @@ from PySide6.QtWidgets import (
QWidget,
)
from app.database import DATABASE_PATH
# ============================================================
# Database
# ============================================================
class Database:
@staticmethod
def get_connection():
connection = sqlite3.connect(DATABASE_PATH)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys = ON")
return connection
@staticmethod
def get_users():
if not DATABASE_PATH.exists():
return []
connection = Database.get_connection()
try:
return connection.execute(
"""
SELECT *
FROM users
ORDER BY username COLLATE NOCASE
"""
).fetchall()
finally:
connection.close()
@staticmethod
def get_user(user_id):
connection = Database.get_connection()
try:
return connection.execute(
"""
SELECT *
FROM users
WHERE id = ?
""",
(user_id,),
).fetchone()
finally:
connection.close()
@staticmethod
def get_remaining_time(user_id):
if not DATABASE_PATH.exists():
return 0, 0
connection = Database.get_connection()
try:
allowance_row = connection.execute(
"""
SELECT COALESCE(
allowance_seconds,
0
) AS total
FROM daily_allowances
WHERE user_id = ?
AND weekday = CAST(
strftime('%w', 'now', 'localtime')
AS INTEGER
)
""",
(user_id,),
).fetchone()
allowance = (
allowance_row["total"]
if allowance_row
else 0
)
usage_row = connection.execute(
"""
SELECT COALESCE(
used_seconds,
0
) AS total
FROM usage
WHERE user_id = ?
AND date = date('now', 'localtime')
""",
(user_id,),
).fetchone()
used = (
usage_row["total"]
if usage_row
else 0
)
remaining = max(
allowance - used,
0,
)
return remaining, allowance
finally:
connection.close()
@staticmethod
def get_allowances(user_id):
connection = Database.get_connection()
try:
rows = connection.execute(
"""
SELECT weekday, allowance_seconds
FROM daily_allowances
WHERE user_id = ?
""",
(user_id,),
).fetchall()
return {
row["weekday"]: row["allowance_seconds"]
for row in rows
}
finally:
connection.close()
@staticmethod
def get_access_windows(user_id):
connection = Database.get_connection()
try:
rows = connection.execute(
"""
SELECT
weekday,
start_minute,
end_minute
FROM access_windows
WHERE user_id = ?
ORDER BY weekday, start_minute
""",
(user_id,),
).fetchall()
result = {}
for row in rows:
result.setdefault(
row["weekday"],
[],
).append(
(
row["start_minute"],
row["end_minute"],
)
)
return result
finally:
connection.close()
@staticmethod
def create_user(
username,
enabled,
allowances,
access_windows,
):
connection = Database.get_connection()
try:
cursor = connection.execute(
"""
INSERT INTO users (
username,
enabled
)
VALUES (?, ?)
""",
(
username,
1 if enabled else 0,
),
)
user_id = cursor.lastrowid
for weekday, seconds in allowances.items():
connection.execute(
"""
INSERT INTO daily_allowances (
user_id,
weekday,
allowance_seconds
)
VALUES (?, ?, ?)
""",
(
user_id,
weekday,
seconds,
),
)
for weekday, windows in access_windows.items():
for start_minute, end_minute in windows:
connection.execute(
"""
INSERT INTO access_windows (
user_id,
weekday,
start_minute,
end_minute
)
VALUES (?, ?, ?, ?)
""",
(
user_id,
weekday,
start_minute,
end_minute,
),
)
connection.commit()
return user_id
finally:
connection.close()
@staticmethod
def update_user(
user_id,
enabled,
allowances,
access_windows,
):
connection = Database.get_connection()
try:
connection.execute(
"""
UPDATE users
SET enabled = ?
WHERE id = ?
""",
(
1 if enabled else 0,
user_id,
),
)
for weekday, seconds in allowances.items():
connection.execute(
"""
INSERT INTO daily_allowances (
user_id,
weekday,
allowance_seconds
)
VALUES (?, ?, ?)
ON CONFLICT(user_id, weekday)
DO UPDATE SET
allowance_seconds =
excluded.allowance_seconds
""",
(
user_id,
weekday,
seconds,
),
)
# Replace access windows completely.
connection.execute(
"""
DELETE FROM access_windows
WHERE user_id = ?
""",
(user_id,),
)
for weekday, windows in access_windows.items():
for start_minute, end_minute in windows:
connection.execute(
"""
INSERT INTO access_windows (
user_id,
weekday,
start_minute,
end_minute
)
VALUES (?, ?, ?, ?)
""",
(
user_id,
weekday,
start_minute,
end_minute,
),
)
connection.commit()
finally:
connection.close()
@staticmethod
def delete_user(user_id):
connection = Database.get_connection()
try:
connection.execute(
"""
DELETE FROM users
WHERE id = ?
""",
(user_id,),
)
connection.commit()
finally:
connection.close()
@staticmethod
def set_enabled(user_id, enabled):
connection = Database.get_connection()
try:
connection.execute(
"""
UPDATE users
SET enabled = ?
WHERE id = ?
""",
(
1 if enabled else 0,
user_id,
),
)
connection.commit()
finally:
connection.close()
@staticmethod
def add_temporary_time(
user_id,
seconds,
):
connection = Database.get_connection()
try:
connection.execute(
"""
INSERT INTO temporary_grants (
user_id,
seconds,
remaining_seconds
)
VALUES (?, ?, ?)
""",
(
user_id,
seconds,
seconds,
),
)
connection.commit()
finally:
connection.close()
from .client import Database, DatabaseError
# ============================================================
# Linux users
@@ -421,7 +38,6 @@ def linux_user_exists(username):
except KeyError:
return False
# ============================================================
# Helpers
# ============================================================
@@ -1464,7 +1080,7 @@ class UserDialog(QDialog):
access_windows,
)
except sqlite3.IntegrityError:
except DatabaseError:
QMessageBox.warning(
self,