first commit

This commit is contained in:
2026-09-17 03:43:51 +05:00
commit c4f01044e9
15 changed files with 3147 additions and 0 deletions
+817
View File
@@ -0,0 +1,817 @@
from fastapi import (
FastAPI,
HTTPException,
Request,
Form,
)
from fastapi.responses import (
RedirectResponse,
)
from .scheduler import (
start_scheduler,
stop_scheduler,
)
from fastapi.templating import (
Jinja2Templates,
)
from pydantic import BaseModel
from .database import (
initialize_database,
get_db,
)
from .users import (
linux_user_exists,
lock_user,
unlock_user,
terminate_user,
is_locked,
)
app = FastAPI(
title="Parental Control",
version="0.1.0",
)
templates = Jinja2Templates(
directory="templates"
)
WEEKDAYS = [
(0, "Monday"),
(1, "Tuesday"),
(2, "Wednesday"),
(3, "Thursday"),
(4, "Friday"),
(5, "Saturday"),
(6, "Sunday"),
]
@app.on_event("startup")
def startup():
initialize_database()
start_scheduler()
@app.on_event("shutdown")
def shutdown():
stop_scheduler()
class AllowanceRequest(BaseModel):
weekday: int
seconds: int
class WindowRequest(BaseModel):
weekday: int
start_minute: int
end_minute: int
class GrantRequest(BaseModel):
seconds: int
def get_user(user_id: int):
with get_db() as db:
return db.execute(
"""
SELECT id, username, enabled
FROM users
WHERE id = ?
""",
(user_id,)
).fetchone()
@app.get("/")
def root():
return {
"application": "Parental Control",
"version": "0.1.0",
"status": "running"
}
@app.get("/api/users")
def list_users():
with get_db() as db:
rows = db.execute(
"""
SELECT id, username, enabled
FROM users
ORDER BY username
"""
).fetchall()
return [
{
"id": row["id"],
"username": row["username"],
"enabled": bool(row["enabled"]),
"locked": is_locked(row["username"])
}
for row in rows
]
@app.post("/api/users/{user_id}/lock")
def manually_lock(user_id: int):
row = get_user(user_id)
if row is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
lock_user(row["username"])
return {
"username": row["username"],
"locked": True
}
@app.post("/api/users/{user_id}/unlock")
def manually_unlock(user_id: int):
row = get_user(user_id)
if row is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
unlock_user(row["username"])
return {
"username": row["username"],
"locked": False
}
@app.post("/api/users/{user_id}/terminate")
def manually_terminate(user_id: int):
row = get_user(user_id)
if row is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
terminate_user(row["username"])
return {
"username": row["username"],
"terminated": True
}
@app.post("/api/users/{user_id}/allowance")
def set_allowance(
user_id: int,
request: AllowanceRequest
):
if get_user(user_id) is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
if request.weekday < 0 or request.weekday > 6:
raise HTTPException(
status_code=400,
detail="Invalid weekday"
)
if request.seconds < 0:
raise HTTPException(
status_code=400,
detail="Allowance cannot be negative"
)
with get_db() as db:
db.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,
request.weekday,
request.seconds
)
)
return {
"user_id": user_id,
"weekday": request.weekday,
"seconds": request.seconds
}
@app.post("/api/users/{user_id}/windows")
def add_window(
user_id: int,
request: WindowRequest
):
if get_user(user_id) is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
if request.weekday < 0 or request.weekday > 6:
raise HTTPException(
status_code=400,
detail="Invalid weekday"
)
if request.start_minute < 0 or request.start_minute >= 1440:
raise HTTPException(
status_code=400,
detail="Invalid start time"
)
if request.end_minute < 0 or request.end_minute > 1440:
raise HTTPException(
status_code=400,
detail="Invalid end time"
)
if request.end_minute <= request.start_minute:
raise HTTPException(
status_code=400,
detail="End time must be after start time"
)
with get_db() as db:
db.execute(
"""
INSERT INTO access_windows
(
user_id,
weekday,
start_minute,
end_minute
)
VALUES (?, ?, ?, ?)
""",
(
user_id,
request.weekday,
request.start_minute,
request.end_minute
)
)
return {
"status": "created"
}
@app.delete("/api/windows/{window_id}")
def delete_window(window_id: int):
with get_db() as db:
cursor = db.execute(
"""
DELETE FROM access_windows
WHERE id = ?
""",
(window_id,)
)
if cursor.rowcount == 0:
raise HTTPException(
status_code=404,
detail="Window not found"
)
return {
"status": "deleted"
}
@app.post("/api/users/{user_id}/grant")
def grant_time(
user_id: int,
request: GrantRequest
):
if get_user(user_id) is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
if request.seconds <= 0:
raise HTTPException(
status_code=400,
detail="Grant must be greater than zero"
)
with get_db() as db:
db.execute(
"""
INSERT INTO temporary_grants
(
user_id,
seconds,
remaining_seconds
)
VALUES (?, ?, ?)
""",
(
user_id,
request.seconds,
request.seconds
)
)
return {
"user_id": user_id,
"seconds": request.seconds
}
@app.get("/admin")
def admin_page(
request: Request
):
with get_db() as db:
users = db.execute(
"""
SELECT id, username, enabled
FROM users
ORDER BY username
"""
).fetchall()
return templates.TemplateResponse(
request=request,
name="index.html",
context={
"users": users
}
)
@app.get("/admin/users/{user_id}")
def admin_user_page(
request: Request,
user_id: int
):
user = get_user(user_id)
if user is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
with get_db() as db:
allowances = db.execute(
"""
SELECT weekday, allowance_seconds
FROM daily_allowances
WHERE user_id = ?
ORDER BY weekday
""",
(user_id,)
).fetchall()
windows = db.execute(
"""
SELECT
id,
weekday,
start_minute,
end_minute
FROM access_windows
WHERE user_id = ?
ORDER BY weekday, start_minute
""",
(user_id,)
).fetchall()
grants = db.execute(
"""
SELECT
id,
seconds,
created_at,
expires_at,
consumed
FROM temporary_grants
WHERE user_id = ?
ORDER BY id DESC
LIMIT 20
""",
(user_id,)
).fetchall()
allowance_map = {
row["weekday"]: row["allowance_seconds"]
for row in allowances
}
return templates.TemplateResponse(
request=request,
name="user.html",
context={
"user": user,
"locked": is_locked(user["username"]),
"weekdays": WEEKDAYS,
"allowances": allowance_map,
"windows": windows,
"grants": grants,
}
)
@app.post("/admin/users/{user_id}/allowance")
def admin_set_allowance(
user_id: int,
weekday: int = Form(...),
hours: int = Form(...),
minutes: int = Form(...),
):
if get_user(user_id) is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
if weekday < 0 or weekday > 6:
raise HTTPException(
status_code=400,
detail="Invalid weekday"
)
if hours < 0 or minutes < 0 or minutes > 59:
raise HTTPException(
status_code=400,
detail="Invalid time"
)
seconds = (hours * 3600) + (minutes * 60)
with get_db() as db:
db.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
)
)
return RedirectResponse(
f"/admin/users/{user_id}",
status_code=303
)
@app.post("/admin/users/{user_id}/window")
def admin_add_window(
user_id: int,
weekday: int = Form(...),
start_time: str = Form(...),
end_time: str = Form(...),
):
if get_user(user_id) is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
try:
start_hour, start_minute = map(
int,
start_time.split(":")
)
end_hour, end_minute = map(
int,
end_time.split(":")
)
except ValueError:
raise HTTPException(
status_code=400,
detail="Invalid time format"
)
start_total = (
start_hour * 60
+ start_minute
)
end_total = (
end_hour * 60
+ end_minute
)
if weekday < 0 or weekday > 6:
raise HTTPException(
status_code=400,
detail="Invalid weekday"
)
if start_total < 0 or start_total >= 1440:
raise HTTPException(
status_code=400,
detail="Invalid start time"
)
if end_total <= start_total:
raise HTTPException(
status_code=400,
detail="End time must be after start time"
)
with get_db() as db:
db.execute(
"""
INSERT INTO access_windows
(
user_id,
weekday,
start_minute,
end_minute
)
VALUES (?, ?, ?, ?)
""",
(
user_id,
weekday,
start_total,
end_total
)
)
return RedirectResponse(
f"/admin/users/{user_id}",
status_code=303
)
@app.post("/admin/windows/{window_id}/delete")
def admin_delete_window(
window_id: int
):
with get_db() as db:
row = db.execute(
"""
SELECT user_id
FROM access_windows
WHERE id = ?
""",
(window_id,)
).fetchone()
if row is None:
raise HTTPException(
status_code=404,
detail="Window not found"
)
user_id = row["user_id"]
db.execute(
"""
DELETE FROM access_windows
WHERE id = ?
""",
(window_id,)
)
return RedirectResponse(
f"/admin/users/{user_id}",
status_code=303
)
@app.post("/admin/users/{user_id}/grant")
def admin_grant_time(
user_id: int,
hours: int = Form(...),
minutes: int = Form(...),
):
if get_user(user_id) is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
if hours < 0 or minutes < 0 or minutes > 59:
raise HTTPException(
status_code=400,
detail="Invalid time"
)
seconds = (
hours * 3600
+ minutes * 60
)
if seconds <= 0:
raise HTTPException(
status_code=400,
detail="Grant must be greater than zero"
)
with get_db() as db:
db.execute(
"""
INSERT INTO temporary_grants
(
user_id,
seconds,
remaining_seconds
)
VALUES (?, ?, ?)
""",
(
user_id,
seconds,
seconds
)
)
return RedirectResponse(
f"/admin/users/{user_id}",
status_code=303
)
@app.post("/admin/users/{user_id}/lock")
def admin_lock_user(
user_id: int
):
row = get_user(user_id)
if row is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
lock_user(row["username"])
return RedirectResponse(
f"/admin/users/{user_id}",
status_code=303
)
@app.post("/admin/users/{user_id}/unlock")
def admin_unlock_user(
user_id: int
):
row = get_user(user_id)
if row is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
unlock_user(row["username"])
return RedirectResponse(
f"/admin/users/{user_id}",
status_code=303
)
@app.post("/admin/users/{user_id}/terminate")
def admin_terminate_user(
user_id: int
):
row = get_user(user_id)
if row is None:
raise HTTPException(
status_code=404,
detail="User not found"
)
terminate_user(row["username"])
return RedirectResponse(
f"/admin/users/{user_id}",
status_code=303
)
@app.post("/admin/users/{user_id}/delete")
def delete_user(
user_id: int
):
with get_db() as db:
db.execute(
"""
DELETE FROM users
WHERE id = ?
""",
(user_id,)
)
return RedirectResponse(
"/admin",
status_code=303
)
@app.post("/admin/users")
def admin_add_user(
username: str = Form(...)
):
username = username.strip()
if not linux_user_exists(username):
raise HTTPException(
status_code=400,
detail="Linux user does not exist"
)
with get_db() as db:
existing = db.execute(
"""
SELECT id
FROM users
WHERE username = ?
""",
(username,)
).fetchone()
if existing is not None:
raise HTTPException(
status_code=400,
detail="User is already configured"
)
cursor = db.execute(
"""
INSERT INTO users(username)
VALUES(?)
""",
(username,)
)
user_id = cursor.lastrowid
for weekday in range(7):
db.execute(
"""
INSERT INTO daily_allowances
(
user_id,
weekday,
allowance_seconds
)
VALUES (?, ?, ?)
""",
(
user_id,
weekday,
0
)
)
return RedirectResponse(
"/admin",
status_code=303
)