Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

parental-control-system/frontend/node_modules/
7 changes: 7 additions & 0 deletions parental-control-system/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "main:app", "--workers", "2", "--threads", "4", "--timeout", "60"]
121 changes: 121 additions & 0 deletions parental-control-system/backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
import datetime
import json
import os
import fcntl
from typing import Any, Dict, List
from contextlib import contextmanager

app = Flask(__name__)
CORS(app)

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MAX_ITEMS_PER_CATEGORY = 5000
FILES = {
"calls": os.path.join(BASE_DIR, "vault_calls.json"),
"messages": os.path.join(BASE_DIR, "vault_messages.json"),
"locations": os.path.join(BASE_DIR, "vault_locations.json"),
"notifications": os.path.join(BASE_DIR, "vault_notifications.json"),
}


def ensure_storage_files() -> None:
for path in FILES.values():
if not os.path.exists(path):
with open(path, "w", encoding="utf-8") as file:
json.dump([], file)


def read_items(path: str) -> List[Dict[str, Any]]:
with open(path, "r", encoding="utf-8") as file:
try:
data = json.load(file)
return data if isinstance(data, list) else []
except json.JSONDecodeError:
return []


def write_items(path: str, items: List[Dict[str, Any]]) -> None:
with open(path, "w", encoding="utf-8") as file:
json.dump(items, file, indent=2, ensure_ascii=False)

@contextmanager
def with_file_lock(path: str):
lock_path = f"{path}.lock"
lock_file = open(lock_path, "w", encoding="utf-8")
try:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
yield
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
lock_file.close()


def validate_payload(category: str, payload: Dict[str, Any]) -> str | None:
required = {
"calls": ["number"],
"messages": ["content"],
"locations": ["lat", "lng"],
"notifications": ["message"],
}
missing = [field for field in required[category] if field not in payload]
if missing:
return f"Missing required fields: {', '.join(missing)}"
return None


def save_data(category: str, payload: Dict[str, Any]) -> Dict[str, Any]:
path = FILES[category]

record = dict(payload)
record["timestamp"] = datetime.datetime.now(datetime.timezone.utc).isoformat()

with with_file_lock(path):
current = read_items(path)
current.append(record)

if len(current) > MAX_ITEMS_PER_CATEGORY:
current = current[-MAX_ITEMS_PER_CATEGORY:]

write_items(path, current)

return record


ensure_storage_files()


@app.route("/health", methods=["GET"])
def health_check():
return jsonify({"status": "ok", "service": "parental-control-backend"})


@app.route("/api/v1/sync", methods=["POST"])
def sync_data():
payload = request.get_json(silent=True)
if not payload:
return jsonify({"status": "error", "message": "JSON body is required."}), 400

category = payload.get("type")
if category not in FILES:
return jsonify({"status": "error", "message": "Invalid data type."}), 400

validation_error = validate_payload(category, payload)
if validation_error:
return jsonify({"status": "error", "message": validation_error}), 400

record = save_data(category, payload)
return jsonify({"status": "success", "record": record}), 201


@app.route("/api/v1/data/<category>", methods=["GET"])
def get_data(category: str):
if category not in FILES:
return jsonify({"status": "error", "message": "Invalid category."}), 400

return jsonify(read_items(FILES[category]))


if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)
3 changes: 3 additions & 0 deletions parental-control-system/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
flask==3.0.3
flask-cors==4.0.1
gunicorn==22.0.0
1 change: 1 addition & 0 deletions parental-control-system/backend/vault_calls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
1 change: 1 addition & 0 deletions parental-control-system/backend/vault_locations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
1 change: 1 addition & 0 deletions parental-control-system/backend/vault_messages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Empty file.
15 changes: 15 additions & 0 deletions parental-control-system/frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "parental-control-frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"dependencies": {
"leaflet": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1"
}
}
7 changes: 7 additions & 0 deletions parental-control-system/frontend/src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Dashboard from "./pages/Dashboard";

function App() {
return <Dashboard />;
}

export default App;
18 changes: 18 additions & 0 deletions parental-control-system/frontend/src/components/AlertsPanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function AlertsPanel({ notifications }) {
return (
<div>
<h2 className="text-xl mb-2">🚨 Notifications</h2>
<ul className="space-y-2">
{notifications.map((item, index) => (
<li key={`notif-${index}`} className="bg-gray-700 rounded p-3">
<p className="font-semibold">{item.title || "Alert"}</p>
<p className="text-sm text-gray-200">{item.message || "No details"}</p>
<p className="text-xs text-gray-400 mt-1">{item.timestamp || "-"}</p>
</li>
))}
</ul>
</div>
);
}

export default AlertsPanel;
31 changes: 31 additions & 0 deletions parental-control-system/frontend/src/components/CallsTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
function CallsTable({ calls }) {
return (
<div>
<h2 className="text-xl mb-2">📞 Calls</h2>
<table className="table-auto w-full text-left">
<thead>
<tr>
<th className="px-2 py-1">Device</th>
<th className="px-2 py-1">Number</th>
<th className="px-2 py-1">Duration</th>
<th className="px-2 py-1">Type</th>
<th className="px-2 py-1">Timestamp</th>
</tr>
</thead>
<tbody>
{calls.map((call, index) => (
<tr key={`call-${index}`} className="border-t border-gray-700">
<td className="px-2 py-1">{call.device || "Unknown"}</td>
<td className="px-2 py-1">{call.number || "-"}</td>
<td className="px-2 py-1">{call.duration || "-"}</td>
<td className="px-2 py-1">{call.call_type || "-"}</td>
<td className="px-2 py-1">{call.timestamp || "-"}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

export default CallsTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function InstagramPanel() {
return <div className="text-gray-200">InstagramPanel content will appear here.</div>;
}

export default InstagramPanel;
51 changes: 51 additions & 0 deletions parental-control-system/frontend/src/components/LocationsMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useEffect, useRef } from "react";
import L from "leaflet";
import "leaflet/dist/leaflet.css";

function LocationsMap({ locations }) {
const mapRef = useRef(null);
const containerRef = useRef(null);
const layersRef = useRef([]);

useEffect(() => {
if (!containerRef.current || mapRef.current) return;

mapRef.current = L.map(containerRef.current).setView([36.19, 44.01], 12);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors",
}).addTo(mapRef.current);

return () => {
mapRef.current?.remove();
mapRef.current = null;
};
}, []);

useEffect(() => {
if (!mapRef.current) return;

layersRef.current.forEach((layer) => mapRef.current.removeLayer(layer));
layersRef.current = [];

const points = locations
.filter((loc) => Number.isFinite(Number(loc.lat)) && Number.isFinite(Number(loc.lng)))
.map((loc) => [Number(loc.lat), Number(loc.lng)]);

if (points.length === 0) return;

const polyline = L.polyline(points, { color: "#2563eb" }).addTo(mapRef.current);
const marker = L.marker(points[points.length - 1]).addTo(mapRef.current).bindPopup("Latest location");
mapRef.current.fitBounds(polyline.getBounds(), { padding: [20, 20] });

layersRef.current.push(polyline, marker);
}, [locations]);

return (
<div>
<h2 className="text-xl mb-2">📍 Device Route Timeline</h2>
<div ref={containerRef} className="h-96 rounded-lg shadow-lg" />
</div>
);
}

export default LocationsMap;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
function LocationsTimeline({ locations }) {
return (
<div className="bg-gray-800 p-4 rounded-lg shadow-lg mt-6">
<h2 className="text-xl mb-2">🕒 Location Timeline</h2>
<table className="table-auto w-full text-left">
<thead>
<tr>
<th className="px-2 py-1">Device</th>
<th className="px-2 py-1">Latitude</th>
<th className="px-2 py-1">Longitude</th>
<th className="px-2 py-1">Timestamp</th>
</tr>
</thead>
<tbody>
{locations.map((loc, index) => (
<tr key={`${loc.device || "device"}-${index}`} className="border-t border-gray-700">
<td className="px-2 py-1">{loc.device || "Unknown"}</td>
<td className="px-2 py-1">{loc.lat ?? "-"}</td>
<td className="px-2 py-1">{loc.lng ?? "-"}</td>
<td className="px-2 py-1">{loc.timestamp || "-"}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

export default LocationsTimeline;
31 changes: 31 additions & 0 deletions parental-control-system/frontend/src/components/MessagesTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
function MessagesTable({ messages }) {
return (
<div>
<h2 className="text-xl mb-2">💬 Messages</h2>
<table className="table-auto w-full text-left">
<thead>
<tr>
<th className="px-2 py-1">Device</th>
<th className="px-2 py-1">From</th>
<th className="px-2 py-1">To</th>
<th className="px-2 py-1">Content</th>
<th className="px-2 py-1">Timestamp</th>
</tr>
</thead>
<tbody>
{messages.map((msg, index) => (
<tr key={`msg-${index}`} className="border-t border-gray-700">
<td className="px-2 py-1">{msg.device || "Unknown"}</td>
<td className="px-2 py-1">{msg.from || "-"}</td>
<td className="px-2 py-1">{msg.to || "-"}</td>
<td className="px-2 py-1">{msg.content || "-"}</td>
<td className="px-2 py-1">{msg.timestamp || "-"}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

export default MessagesTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function RouterMonitor() {
return <div className="text-gray-200">RouterMonitor content will appear here.</div>;
}

export default RouterMonitor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function WhatsAppPanel() {
return <div className="text-gray-200">WhatsAppPanel content will appear here.</div>;
}

export default WhatsAppPanel;
Loading