vumichien commited on
Commit
7e6b994
·
1 Parent(s): ec57f71
Files changed (9) hide show
  1. admin.py +260 -0
  2. api.py +224 -0
  3. app.py +9 -403
  4. database.db +0 -0
  5. database.py +16 -0
  6. models.py +2 -4
  7. templates/admin.html +211 -60
  8. templates/admin_data.html +175 -0
  9. templates/map.html +33 -5
admin.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, Request, Form, Query, status
2
+ from fastapi.responses import HTMLResponse, RedirectResponse
3
+ from sqlalchemy.orm import Session
4
+ from models import User, Device, SystemSetting, StatusRecord
5
+ from database import get_db
6
+ from datetime import datetime
7
+ from typing import Optional
8
+ from fastapi.templating import Jinja2Templates
9
+
10
+ admin_router = APIRouter(prefix="/admin", tags=["admin"])
11
+ templates = Jinja2Templates(directory="templates")
12
+
13
+ def login_required(request: Request, db: Session = Depends(get_db)):
14
+ username = request.cookies.get("username")
15
+ if not username:
16
+ return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
17
+ user = db.query(User).filter(User.username == username).first()
18
+ if not user:
19
+ return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
20
+ return user
21
+
22
+ @admin_router.get("", response_class=HTMLResponse)
23
+ async def admin_page(request: Request, db: Session = Depends(get_db)):
24
+ current_user = login_required(request, db)
25
+ if isinstance(current_user, RedirectResponse):
26
+ return current_user
27
+ if not current_user.is_admin:
28
+ raise HTTPException(
29
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
30
+ )
31
+ users = db.query(User).all()
32
+ devices = db.query(Device).all()
33
+ system_setting = db.query(SystemSetting).first()
34
+ return templates.TemplateResponse(
35
+ "admin.html", {
36
+ "request": request,
37
+ "users": users,
38
+ "devices": devices,
39
+ "system_setting": system_setting,
40
+ "current_user": current_user
41
+ }
42
+ )
43
+
44
+ @admin_router.post("/edit_system_setting")
45
+ async def edit_system_setting(
46
+ request: Request,
47
+ check_connect_period: int = Form(...),
48
+ data_sync_period: int = Form(...),
49
+ get_config_period: int = Form(...),
50
+ point_distance: int = Form(...),
51
+ db: Session = Depends(get_db),
52
+ ):
53
+ current_user = login_required(request, db)
54
+ if isinstance(current_user, RedirectResponse):
55
+ return current_user
56
+ if not current_user.is_admin:
57
+ raise HTTPException(
58
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
59
+ )
60
+
61
+ system_setting = db.query(SystemSetting).first()
62
+ if not system_setting:
63
+ system_setting = SystemSetting()
64
+ db.add(system_setting)
65
+
66
+ system_setting.check_connect_period = check_connect_period
67
+ system_setting.data_sync_period = data_sync_period
68
+ system_setting.get_config_period = get_config_period
69
+ system_setting.point_distance = point_distance
70
+
71
+ db.commit()
72
+ return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
73
+
74
+ @admin_router.get("/data", response_class=HTMLResponse, name="admin_data")
75
+ async def admin_data(
76
+ request: Request,
77
+ start_date: Optional[str] = Query(None),
78
+ end_date: Optional[str] = Query(None),
79
+ device_id: Optional[str] = Query(None),
80
+ page: int = Query(1, ge=1),
81
+ per_page: int = Query(10, ge=10, le=100),
82
+ db: Session = Depends(get_db)
83
+ ):
84
+ current_user = login_required(request, db)
85
+ if isinstance(current_user, RedirectResponse):
86
+ return current_user
87
+ if not current_user.is_admin:
88
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
89
+
90
+ query = db.query(StatusRecord)
91
+
92
+ if start_date:
93
+ query = query.filter(StatusRecord.timestamp >= datetime.strptime(start_date, "%Y-%m-%d"))
94
+ if end_date:
95
+ query = query.filter(StatusRecord.timestamp <= datetime.strptime(end_date, "%Y-%m-%d"))
96
+ if device_id:
97
+ query = query.filter(StatusRecord.device_id == device_id)
98
+
99
+ total_records = query.count()
100
+ total_pages = (total_records + per_page - 1) // per_page
101
+
102
+ records = query.order_by(StatusRecord.timestamp.desc()).offset((page - 1) * per_page).limit(per_page).all()
103
+
104
+ devices = db.query(Device.device_id).distinct().all()
105
+ device_ids = [device.device_id for device in devices]
106
+
107
+ # Tính toán phạm vi trang để hiển thị
108
+ page_range = 5
109
+ start_page = max(1, page - page_range // 2)
110
+ end_page = min(total_pages, start_page + page_range - 1)
111
+ start_page = max(1, end_page - page_range + 1)
112
+
113
+ return templates.TemplateResponse(
114
+ "admin_data.html",
115
+ {
116
+ "request": request,
117
+ "records": records,
118
+ "current_page": page,
119
+ "total_pages": total_pages,
120
+ "start_page": start_page,
121
+ "end_page": end_page,
122
+ "start_date": start_date,
123
+ "end_date": end_date,
124
+ "device_id": device_id,
125
+ "device_ids": device_ids,
126
+ "current_user": current_user,
127
+ "per_page": per_page,
128
+ }
129
+ )
130
+
131
+ @admin_router.post("/delete/{username}")
132
+ async def delete_user(request: Request, username: str, db: Session = Depends(get_db)):
133
+ current_user = login_required(request, db)
134
+ if isinstance(current_user, RedirectResponse):
135
+ return current_user
136
+ if not current_user.is_admin:
137
+ raise HTTPException(
138
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
139
+ )
140
+ user = db.query(User).filter(User.username == username).first()
141
+ if user:
142
+ db.delete(user)
143
+ db.commit()
144
+ return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
145
+
146
+
147
+ @admin_router.post("/edit/{username}")
148
+ async def edit_user(
149
+ request: Request,
150
+ username: str,
151
+ new_username: str = Form(...),
152
+ email: str = Form(...),
153
+ is_admin: bool = Form(False),
154
+ is_active: bool = Form(False),
155
+ db: Session = Depends(get_db),
156
+ ):
157
+ current_user = login_required(request, db)
158
+ if isinstance(current_user, RedirectResponse):
159
+ return current_user
160
+ if not current_user.is_admin:
161
+ raise HTTPException(
162
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
163
+ )
164
+ user = db.query(User).filter(User.username == username).first()
165
+ if user:
166
+ if (
167
+ new_username != username
168
+ and db.query(User).filter(User.username == new_username).first()
169
+ ):
170
+ raise HTTPException(status_code=400, detail="Username already exists")
171
+ user.username = new_username
172
+ user.email = email
173
+ user.is_admin = is_admin
174
+ user.is_active = is_active
175
+ db.commit()
176
+ return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
177
+
178
+
179
+ @admin_router.post("/add_device")
180
+ async def add_device(
181
+ request: Request,
182
+ name: str = Form(...),
183
+ description: str = Form(...),
184
+ device_id: str = Form(...),
185
+ password: str = Form(...),
186
+ db: Session = Depends(get_db),
187
+ ):
188
+ current_user = login_required(request, db)
189
+ if isinstance(current_user, RedirectResponse):
190
+ return current_user
191
+ if not current_user.is_admin:
192
+ raise HTTPException(
193
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
194
+ )
195
+
196
+ existing_device = db.query(Device).filter(Device.device_id == device_id).first()
197
+ if existing_device:
198
+ raise HTTPException(status_code=400, detail="Device ID already exists")
199
+
200
+ new_device = Device(
201
+ name=name, description=description, device_id=device_id, password=password
202
+ )
203
+ db.add(new_device)
204
+ db.commit()
205
+ return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
206
+
207
+
208
+ @admin_router.post("/edit_device/{device_id}")
209
+ async def edit_device(
210
+ request: Request,
211
+ device_id: str,
212
+ name: str = Form(...),
213
+ description: str = Form(...),
214
+ new_device_id: str = Form(...),
215
+ password: str = Form(...),
216
+ db: Session = Depends(get_db),
217
+ ):
218
+ current_user = login_required(request, db)
219
+ if isinstance(current_user, RedirectResponse):
220
+ return current_user
221
+ if not current_user.is_admin:
222
+ raise HTTPException(
223
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
224
+ )
225
+
226
+ device = db.query(Device).filter(Device.device_id == device_id).first()
227
+ if not device:
228
+ raise HTTPException(status_code=404, detail="Device not found")
229
+
230
+ if (
231
+ new_device_id != device_id
232
+ and db.query(Device).filter(Device.device_id == new_device_id).first()
233
+ ):
234
+ raise HTTPException(status_code=400, detail="New Device ID already exists")
235
+
236
+ device.name = name
237
+ device.description = description
238
+ device.device_id = new_device_id
239
+ device.password = password
240
+ db.commit()
241
+ return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
242
+
243
+
244
+ @admin_router.post("/delete_device/{device_id}")
245
+ async def delete_device(
246
+ request: Request, device_id: str, db: Session = Depends(get_db)
247
+ ):
248
+ current_user = login_required(request, db)
249
+ if isinstance(current_user, RedirectResponse):
250
+ return current_user
251
+ if not current_user.is_admin:
252
+ raise HTTPException(
253
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
254
+ )
255
+
256
+ device = db.query(Device).filter(Device.device_id == device_id).first()
257
+ if device:
258
+ db.delete(device)
259
+ db.commit()
260
+ return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
api.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, Header, status
2
+ from fastapi.responses import JSONResponse
3
+ from sqlalchemy.orm import Session
4
+ from models import StatusRecord, Device, StatusRecordBatch, SystemSetting
5
+ from database import get_db
6
+ from datetime import datetime, timedelta
7
+ import uuid as uuid_module
8
+ import random
9
+ from sqlalchemy.exc import IntegrityError
10
+ from typing import Dict
11
+
12
+ api_router = APIRouter(prefix="/api", tags=["api"])
13
+
14
+ def authenticate_device(device_id: str, device_password: str, db: Session = Depends(get_db)):
15
+ device = db.query(Device).filter(Device.device_id == device_id).first()
16
+ if not device or device.password != device_password:
17
+ raise HTTPException(
18
+ status_code=status.HTTP_401_UNAUTHORIZED,
19
+ detail="Invalid device credentials",
20
+ )
21
+ return device
22
+
23
+ @api_router.post("/generate-data")
24
+ def generate_data(
25
+ device_id: str = Header(...),
26
+ device_password: str = Header(...),
27
+ db: Session = Depends(get_db),
28
+ ):
29
+ authenticate_device(device_id, device_password, db)
30
+ base_latitude = 35.6837
31
+ base_longitude = 139.6805
32
+ start_date = datetime(2024, 8, 1)
33
+ end_date = datetime(2024, 8, 7)
34
+ delta = end_date - start_date
35
+
36
+ for _ in range(100):
37
+ random_days = random.randint(0, delta.days)
38
+ random_seconds = random.randint(0, 86400)
39
+ random_time = start_date + timedelta(days=random_days, seconds=random_seconds)
40
+
41
+ random_latitude = base_latitude + random.uniform(-0.01, 0.01)
42
+ random_longitude = base_longitude + random.uniform(-0.01, 0.01)
43
+ random_connect_status = random.choice([0, 1])
44
+
45
+ status_record = StatusRecord(
46
+ device_id=device_id,
47
+ latitude=random_latitude,
48
+ longitude=random_longitude,
49
+ timestamp=random_time,
50
+ connect_status=random_connect_status,
51
+ )
52
+ db.add(status_record)
53
+
54
+ db.commit()
55
+ return {"message": "Demo data generated successfully"}
56
+
57
+
58
+ @api_router.delete("/delete-data", summary="Delete all status records")
59
+ def delete_all_data(
60
+ device_id: str = Header(...),
61
+ device_password: str = Header(...),
62
+ db: Session = Depends(get_db),
63
+ ):
64
+ """
65
+ Delete all status records from the database.
66
+ Requires device authentication.
67
+ """
68
+ authenticate_device(device_id, device_password, db)
69
+ try:
70
+ db.query(StatusRecord).delete()
71
+ db.commit()
72
+ return {"message": "All data deleted successfully"}
73
+ except Exception as e:
74
+ db.rollback()
75
+ raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
76
+
77
+
78
+ @api_router.delete(
79
+ "/delete-data/{device_id}", summary="Delete status records for a specific device"
80
+ )
81
+ def delete_device_data(
82
+ device_id: str,
83
+ auth_device_id: str = Header(...),
84
+ device_password: str = Header(...),
85
+ db: Session = Depends(get_db),
86
+ ):
87
+ """
88
+ Delete status records for a specific device ID.
89
+ Requires device authentication.
90
+ """
91
+ authenticate_device(auth_device_id, device_password, db)
92
+ try:
93
+ deleted_count = (
94
+ db.query(StatusRecord).filter(StatusRecord.device_id == device_id).delete()
95
+ )
96
+ db.commit()
97
+ if deleted_count == 0:
98
+ return {"message": f"No data found for device ID: {device_id}"}
99
+ return {"message": f"Data for device ID {device_id} deleted successfully"}
100
+ except Exception as e:
101
+ db.rollback()
102
+ raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
103
+
104
+
105
+ @api_router.post("/upload_batch")
106
+ async def upload_data_batch(
107
+ records: StatusRecordBatch,
108
+ device_id: str = Header(...),
109
+ device_password: str = Header(...),
110
+ db: Session = Depends(get_db),
111
+ ):
112
+ """
113
+ Upload multiple status records in a single request.
114
+ Requires device authentication and unique UUIDs for each record.
115
+ Uses the device_id from the header for all records.
116
+ """
117
+ authenticate_device(device_id, device_password, db)
118
+
119
+ successful_uploads = 0
120
+ failed_uploads = 0
121
+ error_messages = []
122
+ failed_records = []
123
+
124
+ for record in records.records:
125
+ try:
126
+ # Validate UUID
127
+ uuid_obj = uuid_module.UUID(record.uuid)
128
+
129
+ # Validate timestamp
130
+ timestamp_dt = datetime.strptime(record.timestamp, "%Y-%m-%d %H:%M:%S")
131
+
132
+ status_record = StatusRecord(
133
+ uuid=str(uuid_obj),
134
+ device_id=device_id,
135
+ latitude=record.latitude,
136
+ longitude=record.longitude,
137
+ timestamp=timestamp_dt,
138
+ connect_status=record.connect_status,
139
+ )
140
+ db.add(status_record)
141
+ successful_uploads += 1
142
+ except ValueError as ve:
143
+ failed_uploads += 1
144
+ error_messages.append(f"Invalid data format: {str(ve)}")
145
+ failed_records.append(str(uuid_obj))
146
+ except IntegrityError:
147
+ db.rollback()
148
+ failed_uploads += 1
149
+ error_messages.append(f"Duplicate UUID: {record.uuid}")
150
+ failed_records.append(str(uuid_obj))
151
+ except Exception as e:
152
+ db.rollback()
153
+ failed_uploads += 1
154
+ error_messages.append(f"Error processing record: {str(e)}")
155
+ failed_records.append(str(uuid_obj))
156
+ try:
157
+ db.commit()
158
+ except Exception as e:
159
+ db.rollback()
160
+ return JSONResponse(
161
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
162
+ content={"message": f"Error committing to database: {str(e)}"},
163
+ )
164
+
165
+ return JSONResponse(
166
+ status_code=status.HTTP_200_OK,
167
+ content={
168
+ "status": "ok",
169
+ "message": "Batch upload completed",
170
+ "successful_uploads": successful_uploads,
171
+ "failed_uploads": failed_uploads,
172
+ "errors": error_messages,
173
+ "failed_records": failed_records,
174
+ },
175
+ )
176
+
177
+
178
+ @api_router.get("/health_check", summary="Check if the API is functioning correctly")
179
+ def health_check(
180
+ device_id: str = Header(...),
181
+ device_password: str = Header(...),
182
+ db: Session = Depends(get_db),
183
+ ):
184
+ """
185
+ Perform a health check on the API.
186
+ Requires device authentication.
187
+ Returns a 200 status code if successful.
188
+ Returns a 401 Unauthorized error if authentication fails.
189
+ """
190
+ try:
191
+ authenticate_device(device_id, device_password, db)
192
+ return JSONResponse(content={"status": "ok"}, status_code=status.HTTP_200_OK)
193
+ except HTTPException as e:
194
+ if e.status_code == status.HTTP_401_UNAUTHORIZED:
195
+ return JSONResponse(
196
+ content={"status": "error", "detail": "Unauthorized"},
197
+ status_code=status.HTTP_401_UNAUTHORIZED,
198
+ )
199
+ raise e
200
+
201
+
202
+ @api_router.get(
203
+ "/config", summary="Get system configuration", response_model=Dict[str, int]
204
+ )
205
+ def get_config(
206
+ device_id: str = Header(...),
207
+ device_password: str = Header(...),
208
+ db: Session = Depends(get_db),
209
+ ):
210
+ """
211
+ Retrieve the system configuration from SystemSetting.
212
+ Requires device authentication.
213
+ """
214
+ authenticate_device(device_id, device_password, db)
215
+ system_setting = db.query(SystemSetting).first()
216
+ if not system_setting:
217
+ raise HTTPException(status_code=404, detail="System settings not found")
218
+
219
+ return {
220
+ "check_connect_period": system_setting.check_connect_period,
221
+ "data_sync_period": system_setting.data_sync_period,
222
+ "get_config_period": system_setting.get_config_period,
223
+ "point_distance": system_setting.point_distance,
224
+ }
app.py CHANGED
@@ -1,44 +1,19 @@
1
- from fastapi import (
2
- FastAPI,
3
- HTTPException,
4
- Request,
5
- Form,
6
- Depends,
7
- status,
8
- APIRouter,
9
- Header,
10
- )
11
- from fastapi.responses import (
12
- HTMLResponse,
13
- RedirectResponse,
14
- JSONResponse,
15
- StreamingResponse,
16
- )
17
  from fastapi.templating import Jinja2Templates
18
- from fastapi.staticfiles import StaticFiles
19
  from fastapi.security import HTTPBasic
20
- from datetime import datetime, timedelta
21
- import random
22
  import folium
23
- import uuid as uuid_module
24
  from folium.plugins import MarkerCluster
25
  from typing import Optional
26
- from sqlalchemy import create_engine
27
- from sqlalchemy.orm import sessionmaker, Session
28
- from models import Base, User, StatusRecord, SystemSetting, Device, StatusRecordBatch
29
  import io
30
  import csv
31
- from typing import Dict
32
-
33
- # Database setup
34
- SQLALCHEMY_DATABASE_URL = "sqlite:///./database.db"
35
- engine = create_engine(
36
- SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
37
- )
38
- SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
39
-
40
- Base.metadata.create_all(bind=engine)
41
-
42
 
43
  # Create default admin user and system settings
44
  def create_default_data():
@@ -74,19 +49,6 @@ app = FastAPI()
74
  templates = Jinja2Templates(directory="templates")
75
  security = HTTPBasic()
76
 
77
- # Create APIRouters for grouping
78
- admin_router = APIRouter(prefix="/admin", tags=["admin"])
79
- api_router = APIRouter(prefix="/api", tags=["api"])
80
-
81
-
82
- # Dependency to get the database session
83
- def get_db():
84
- db = SessionLocal()
85
- try:
86
- yield db
87
- finally:
88
- db.close()
89
-
90
 
91
  def get_current_user(request: Request, db: Session = Depends(get_db)) -> Optional[User]:
92
  username = request.cookies.get("username")
@@ -104,7 +66,6 @@ def login_required(request: Request, db: Session = Depends(get_db)):
104
  return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
105
  return user
106
 
107
-
108
  # Device authentication function
109
  def authenticate_device(
110
  device_id: str, device_password: str, db: Session = Depends(get_db)
@@ -179,360 +140,6 @@ async def register(
179
  return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
180
 
181
 
182
- # Admin routes
183
- @admin_router.get("", response_class=HTMLResponse)
184
- async def admin_page(request: Request, db: Session = Depends(get_db)):
185
- current_user = login_required(request, db)
186
- if isinstance(current_user, RedirectResponse):
187
- return current_user
188
- if not current_user.is_admin:
189
- raise HTTPException(
190
- status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
191
- )
192
- users = db.query(User).all()
193
- devices = db.query(Device).all()
194
- return templates.TemplateResponse(
195
- "admin.html", {"request": request, "users": users, "devices": devices}
196
- )
197
-
198
-
199
- @admin_router.post("/delete/{username}")
200
- async def delete_user(request: Request, username: str, db: Session = Depends(get_db)):
201
- current_user = login_required(request, db)
202
- if isinstance(current_user, RedirectResponse):
203
- return current_user
204
- if not current_user.is_admin:
205
- raise HTTPException(
206
- status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
207
- )
208
- user = db.query(User).filter(User.username == username).first()
209
- if user:
210
- db.delete(user)
211
- db.commit()
212
- return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
213
-
214
-
215
- @admin_router.post("/edit/{username}")
216
- async def edit_user(
217
- request: Request,
218
- username: str,
219
- new_username: str = Form(...),
220
- email: str = Form(...),
221
- is_admin: bool = Form(False),
222
- is_active: bool = Form(False),
223
- db: Session = Depends(get_db),
224
- ):
225
- current_user = login_required(request, db)
226
- if isinstance(current_user, RedirectResponse):
227
- return current_user
228
- if not current_user.is_admin:
229
- raise HTTPException(
230
- status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
231
- )
232
- user = db.query(User).filter(User.username == username).first()
233
- if user:
234
- if (
235
- new_username != username
236
- and db.query(User).filter(User.username == new_username).first()
237
- ):
238
- raise HTTPException(status_code=400, detail="Username already exists")
239
- user.username = new_username
240
- user.email = email
241
- user.is_admin = is_admin
242
- user.is_active = is_active
243
- db.commit()
244
- return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
245
-
246
-
247
- @admin_router.post("/add_device")
248
- async def add_device(
249
- request: Request,
250
- name: str = Form(...),
251
- description: str = Form(...),
252
- device_id: str = Form(...),
253
- password: str = Form(...),
254
- db: Session = Depends(get_db),
255
- ):
256
- current_user = login_required(request, db)
257
- if isinstance(current_user, RedirectResponse):
258
- return current_user
259
- if not current_user.is_admin:
260
- raise HTTPException(
261
- status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
262
- )
263
-
264
- existing_device = db.query(Device).filter(Device.device_id == device_id).first()
265
- if existing_device:
266
- raise HTTPException(status_code=400, detail="Device ID already exists")
267
-
268
- new_device = Device(
269
- name=name, description=description, device_id=device_id, password=password
270
- )
271
- db.add(new_device)
272
- db.commit()
273
- return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
274
-
275
-
276
- @admin_router.post("/edit_device/{device_id}")
277
- async def edit_device(
278
- request: Request,
279
- device_id: str,
280
- name: str = Form(...),
281
- description: str = Form(...),
282
- new_device_id: str = Form(...),
283
- password: str = Form(...),
284
- db: Session = Depends(get_db),
285
- ):
286
- current_user = login_required(request, db)
287
- if isinstance(current_user, RedirectResponse):
288
- return current_user
289
- if not current_user.is_admin:
290
- raise HTTPException(
291
- status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
292
- )
293
-
294
- device = db.query(Device).filter(Device.device_id == device_id).first()
295
- if not device:
296
- raise HTTPException(status_code=404, detail="Device not found")
297
-
298
- if (
299
- new_device_id != device_id
300
- and db.query(Device).filter(Device.device_id == new_device_id).first()
301
- ):
302
- raise HTTPException(status_code=400, detail="New Device ID already exists")
303
-
304
- device.name = name
305
- device.description = description
306
- device.device_id = new_device_id
307
- device.password = password
308
- db.commit()
309
- return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
310
-
311
-
312
- @admin_router.post("/delete_device/{device_id}")
313
- async def delete_device(
314
- request: Request, device_id: str, db: Session = Depends(get_db)
315
- ):
316
- current_user = login_required(request, db)
317
- if isinstance(current_user, RedirectResponse):
318
- return current_user
319
- if not current_user.is_admin:
320
- raise HTTPException(
321
- status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
322
- )
323
-
324
- device = db.query(Device).filter(Device.device_id == device_id).first()
325
- if device:
326
- db.delete(device)
327
- db.commit()
328
- return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
329
-
330
-
331
- # API routes
332
- @api_router.post("/generate-data")
333
- def generate_data(
334
- device_id: str = Header(...),
335
- device_password: str = Header(...),
336
- db: Session = Depends(get_db),
337
- ):
338
- authenticate_device(device_id, device_password, db)
339
- base_latitude = 35.6837
340
- base_longitude = 139.6805
341
- start_date = datetime(2024, 8, 1)
342
- end_date = datetime(2024, 8, 7)
343
- delta = end_date - start_date
344
-
345
- for _ in range(100):
346
- random_days = random.randint(0, delta.days)
347
- random_seconds = random.randint(0, 86400)
348
- random_time = start_date + timedelta(days=random_days, seconds=random_seconds)
349
-
350
- random_latitude = base_latitude + random.uniform(-0.01, 0.01)
351
- random_longitude = base_longitude + random.uniform(-0.01, 0.01)
352
- random_connect_status = random.choice([0, 1])
353
-
354
- status_record = StatusRecord(
355
- device_id=device_id,
356
- latitude=random_latitude,
357
- longitude=random_longitude,
358
- timestamp=random_time,
359
- connect_status=random_connect_status,
360
- )
361
- db.add(status_record)
362
-
363
- db.commit()
364
- return {"message": "Demo data generated successfully"}
365
-
366
-
367
- @api_router.delete("/delete-data", summary="Delete all status records")
368
- def delete_all_data(
369
- device_id: str = Header(...),
370
- device_password: str = Header(...),
371
- db: Session = Depends(get_db),
372
- ):
373
- """
374
- Delete all status records from the database.
375
- Requires device authentication.
376
- """
377
- authenticate_device(device_id, device_password, db)
378
- try:
379
- db.query(StatusRecord).delete()
380
- db.commit()
381
- return {"message": "All data deleted successfully"}
382
- except Exception as e:
383
- db.rollback()
384
- raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
385
-
386
-
387
- @api_router.delete(
388
- "/delete-data/{device_id}", summary="Delete status records for a specific device"
389
- )
390
- def delete_device_data(
391
- device_id: str,
392
- auth_device_id: str = Header(...),
393
- device_password: str = Header(...),
394
- db: Session = Depends(get_db),
395
- ):
396
- """
397
- Delete status records for a specific device ID.
398
- Requires device authentication.
399
- """
400
- authenticate_device(auth_device_id, device_password, db)
401
- try:
402
- deleted_count = (
403
- db.query(StatusRecord).filter(StatusRecord.device_id == device_id).delete()
404
- )
405
- db.commit()
406
- if deleted_count == 0:
407
- return {"message": f"No data found for device ID: {device_id}"}
408
- return {"message": f"Data for device ID {device_id} deleted successfully"}
409
- except Exception as e:
410
- db.rollback()
411
- raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
412
-
413
-
414
- @api_router.post("/upload_batch")
415
- async def upload_data_batch(
416
- records: StatusRecordBatch,
417
- device_id: str = Header(...),
418
- device_password: str = Header(...),
419
- db: Session = Depends(get_db),
420
- ):
421
- """
422
- Upload multiple status records in a single request.
423
- Requires device authentication and unique UUIDs for each record.
424
- Uses the device_id from the header for all records.
425
- """
426
- authenticate_device(device_id, device_password, db)
427
-
428
- successful_uploads = 0
429
- failed_uploads = 0
430
- error_messages = []
431
- failed_records = []
432
-
433
- for record in records.records:
434
- try:
435
- # Validate UUID
436
- uuid_obj = uuid_module.UUID(record.uuid)
437
-
438
- # Validate timestamp
439
- timestamp_dt = datetime.strptime(record.timestamp, "%Y-%m-%d %H:%M:%S")
440
-
441
- status_record = StatusRecord(
442
- uuid=str(uuid_obj),
443
- device_id=device_id,
444
- latitude=record.latitude,
445
- longitude=record.longitude,
446
- timestamp=timestamp_dt,
447
- connect_status=record.connect_status,
448
- )
449
- db.add(status_record)
450
- successful_uploads += 1
451
- except ValueError as ve:
452
- failed_uploads += 1
453
- error_messages.append(f"Invalid data format: {str(ve)}")
454
- failed_records.append(str(uuid_obj))
455
- except IntegrityError:
456
- db.rollback()
457
- failed_uploads += 1
458
- error_messages.append(f"Duplicate UUID: {record.uuid}")
459
- failed_records.append(str(uuid_obj))
460
- except Exception as e:
461
- db.rollback()
462
- failed_uploads += 1
463
- error_messages.append(f"Error processing record: {str(e)}")
464
- failed_records.append(str(uuid_obj))
465
- try:
466
- db.commit()
467
- except Exception as e:
468
- db.rollback()
469
- return JSONResponse(
470
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
471
- content={"message": f"Error committing to database: {str(e)}"},
472
- )
473
-
474
- return JSONResponse(
475
- status_code=status.HTTP_200_OK,
476
- content={
477
- "status": "ok",
478
- "message": "Batch upload completed",
479
- "successful_uploads": successful_uploads,
480
- "failed_uploads": failed_uploads,
481
- "errors": error_messages,
482
- "failed_records": failed_records,
483
- },
484
- )
485
-
486
-
487
- @api_router.get("/health_check", summary="Check if the API is functioning correctly")
488
- def health_check(
489
- device_id: str = Header(...),
490
- device_password: str = Header(...),
491
- db: Session = Depends(get_db),
492
- ):
493
- """
494
- Perform a health check on the API.
495
- Requires device authentication.
496
- Returns a 200 status code if successful.
497
- Returns a 401 Unauthorized error if authentication fails.
498
- """
499
- try:
500
- authenticate_device(device_id, device_password, db)
501
- return JSONResponse(content={"status": "ok"}, status_code=status.HTTP_200_OK)
502
- except HTTPException as e:
503
- if e.status_code == status.HTTP_401_UNAUTHORIZED:
504
- return JSONResponse(
505
- content={"status": "error", "detail": "Unauthorized"},
506
- status_code=status.HTTP_401_UNAUTHORIZED,
507
- )
508
- raise e
509
-
510
-
511
- @api_router.get(
512
- "/config", summary="Get system configuration", response_model=Dict[str, int]
513
- )
514
- def get_config(
515
- device_id: str = Header(...),
516
- device_password: str = Header(...),
517
- db: Session = Depends(get_db),
518
- ):
519
- """
520
- Retrieve the system configuration from SystemSetting.
521
- Requires device authentication.
522
- """
523
- authenticate_device(device_id, device_password, db)
524
- system_setting = db.query(SystemSetting).first()
525
- if not system_setting:
526
- raise HTTPException(status_code=404, detail="System settings not found")
527
-
528
- return {
529
- "check_connect_period": system_setting.check_connect_period,
530
- "data_sync_period": system_setting.data_sync_period,
531
- "get_config_period": system_setting.get_config_period,
532
- "point_distance": system_setting.point_distance,
533
- }
534
-
535
-
536
  @app.get("/", response_class=HTMLResponse)
537
  def show_map(
538
  request: Request,
@@ -633,5 +240,4 @@ app.include_router(api_router)
633
 
634
  if __name__ == "__main__":
635
  import uvicorn
636
-
637
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ from fastapi import FastAPI, HTTPException, Request, Form, Depends, status, APIRouter
2
+ from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from fastapi.templating import Jinja2Templates
 
4
  from fastapi.security import HTTPBasic
5
+ from datetime import datetime
 
6
  import folium
7
+
8
  from folium.plugins import MarkerCluster
9
  from typing import Optional
10
+ from sqlalchemy.orm import Session
11
+ from models import User, StatusRecord, SystemSetting, Device
 
12
  import io
13
  import csv
14
+ from admin import admin_router
15
+ from api import api_router
16
+ from database import get_db, SessionLocal
 
 
 
 
 
 
 
 
17
 
18
  # Create default admin user and system settings
19
  def create_default_data():
 
49
  templates = Jinja2Templates(directory="templates")
50
  security = HTTPBasic()
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  def get_current_user(request: Request, db: Session = Depends(get_db)) -> Optional[User]:
54
  username = request.cookies.get("username")
 
66
  return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
67
  return user
68
 
 
69
  # Device authentication function
70
  def authenticate_device(
71
  device_id: str, device_password: str, db: Session = Depends(get_db)
 
140
  return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
141
 
142
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  @app.get("/", response_class=HTMLResponse)
144
  def show_map(
145
  request: Request,
 
240
 
241
  if __name__ == "__main__":
242
  import uvicorn
 
243
  uvicorn.run(app, host="0.0.0.0", port=7860)
database.db CHANGED
Binary files a/database.db and b/database.db differ
 
database.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.orm import sessionmaker
4
+
5
+ SQLALCHEMY_DATABASE_URL = "sqlite:///./database.db"
6
+ engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
7
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
8
+
9
+ Base = declarative_base()
10
+
11
+ def get_db():
12
+ db = SessionLocal()
13
+ try:
14
+ yield db
15
+ finally:
16
+ db.close()
models.py CHANGED
@@ -1,11 +1,9 @@
1
- from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, Enum
2
- from sqlalchemy.ext.declarative import declarative_base
3
- from datetime import datetime
4
  import uuid
5
  from pydantic import BaseModel
6
  from typing import List
 
7
 
8
- Base = declarative_base()
9
 
10
  class User(Base):
11
  __tablename__ = "users"
 
1
+ from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean
 
 
2
  import uuid
3
  from pydantic import BaseModel
4
  from typing import List
5
+ from database import Base
6
 
 
7
 
8
  class User(Base):
9
  __tablename__ = "users"
templates/admin.html CHANGED
@@ -5,69 +5,175 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Admin Panel - Signal Tracker</title>
7
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  </head>
9
  <body>
10
  <div class="container mt-5">
11
- <h1>Admin Panel</h1>
12
- <table class="table">
13
- <thead>
14
- <tr>
15
- <th>Username</th>
16
- <th>Email</th>
17
- <th>Is Admin</th>
18
- <th>Last Login</th>
19
- <th>Is Active</th>
20
- <th>Actions</th>
21
- </tr>
22
- </thead>
23
- <tbody>
24
- {% for user in users %}
25
- <tr>
26
- <td>{{ user.username }}</td>
27
- <td>{{ user.email }}</td>
28
- <td>{{ user.is_admin }}</td>
29
- <td>{{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') if user.last_login else 'Never' }}</td>
30
- <td>{{ user.is_active }}</td>
31
- <td>
32
- <button class="btn btn-sm btn-primary" onclick="editUser('{{ user.username }}', '{{ user.email }}', {{ user.is_admin | tojson }}, {{ user.is_active | tojson }})">Edit</button>
33
- <form method="post" action="/admin/delete/{{ user.username }}" style="display: inline;">
34
- <button type="submit" class="btn btn-sm btn-danger">Delete</button>
35
- </form>
36
- </td>
37
- </tr>
38
- {% endfor %}
39
- </tbody>
40
- </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
- <h2>Devices</h2>
43
- <button class="btn btn-success mb-3" onclick="addDevice()">Add Device</button>
44
- <table class="table">
45
- <thead>
46
- <tr>
47
- <th>Name</th>
48
- <th>Description</th>
49
- <th>Device ID</th>
50
- <th>Actions</th>
51
- </tr>
52
- </thead>
53
- <tbody>
54
- {% for device in devices %}
55
- <tr>
56
- <td>{{ device.name }}</td>
57
- <td>{{ device.description }}</td>
58
- <td>{{ device.device_id }}</td>
59
- <td>
60
- <button class="btn btn-sm btn-primary" onclick="editDevice('{{ device.name }}', '{{ device.description }}', '{{ device.device_id }}')">Edit</button>
61
- <form method="post" action="/admin/delete_device/{{ device.device_id }}" style="display: inline;">
62
- <button type="submit" class="btn btn-sm btn-danger">Delete</button>
63
- </form>
64
- </td>
65
- </tr>
66
- {% endfor %}
67
- </tbody>
68
- </table>
 
 
 
 
 
 
 
 
 
 
69
 
70
- <a href="/" class="btn btn-secondary">Back to Map</a>
 
 
71
  </div>
72
 
73
  <!-- Edit User Modal -->
@@ -180,6 +286,42 @@
180
  </div>
181
  </div>
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
184
  <script>
185
  function editUser(username, email, isAdmin, isActive) {
@@ -197,15 +339,24 @@
197
  addDeviceModal.show();
198
  }
199
 
200
- function editDevice(name, description, deviceId) {
201
  document.getElementById('editDeviceForm').action = `/admin/edit_device/${deviceId}`;
202
  document.getElementById('editDeviceName').value = name;
203
  document.getElementById('editDeviceDescription').value = description;
204
  document.getElementById('editDeviceId').value = deviceId;
205
- document.getElementById('editDevicePassword').value = ''; // Clear password field for security
206
  var editDeviceModal = new bootstrap.Modal(document.getElementById('editDeviceModal'));
207
  editDeviceModal.show();
208
  }
 
 
 
 
 
 
 
 
 
209
  </script>
210
  </body>
211
  </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Admin Panel - Signal Tracker</title>
7
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <style>
9
+ .user-info {
10
+ position: absolute;
11
+ top: 20px;
12
+ right: 20px;
13
+ background-color: white;
14
+ padding: 10px 15px;
15
+ border-radius: 25px;
16
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
17
+ display: flex;
18
+ align-items: center;
19
+ font-family: Arial, sans-serif;
20
+ font-size: 14px;
21
+ }
22
+ .user-info span {
23
+ font-weight: bold;
24
+ margin-right: 10px;
25
+ }
26
+ .user-info a {
27
+ text-decoration: none;
28
+ color: #007bff;
29
+ padding: 5px 10px;
30
+ border-radius: 15px;
31
+ transition: background-color 0.3s, color 0.3s;
32
+ }
33
+ .user-info a:hover {
34
+ background-color: #007bff;
35
+ color: white;
36
+ }
37
+ .section {
38
+ margin-bottom: 40px;
39
+ }
40
+ .section-header {
41
+ display: flex;
42
+ justify-content: space-between;
43
+ align-items: center;
44
+ margin-bottom: 20px;
45
+ }
46
+ .bottom-nav {
47
+ margin-top: 40px;
48
+ }
49
+ </style>
50
  </head>
51
  <body>
52
  <div class="container mt-5">
53
+ <div class="user-info">
54
+ <span>Welcome, {{ current_user.username }}</span>
55
+ <a href="/logout">Logout</a>
56
+ </div>
57
+
58
+ <h1 class="mb-4">Admin Panel</h1>
59
+
60
+ <ul class="nav nav-tabs mb-4">
61
+ <li class="nav-item">
62
+ <a class="nav-link" href="/admin/data">Data</a>
63
+ </li>
64
+ <li class="nav-item">
65
+ <a class="nav-link active" href="/admin">Settings</a>
66
+ </li>
67
+ </ul>
68
+
69
+ <div class="section">
70
+ <h2>Users</h2>
71
+ <table class="table">
72
+ <thead>
73
+ <tr>
74
+ <th>Username</th>
75
+ <th>Email</th>
76
+ <th>Is Admin</th>
77
+ <th>Last Login</th>
78
+ <th>Is Active</th>
79
+ <th>Actions</th>
80
+ </tr>
81
+ </thead>
82
+ <tbody>
83
+ {% for user in users %}
84
+ <tr>
85
+ <td>{{ user.username }}</td>
86
+ <td>{{ user.email }}</td>
87
+ <td>{{ user.is_admin }}</td>
88
+ <td>{{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') if user.last_login else 'Never' }}</td>
89
+ <td>{{ user.is_active }}</td>
90
+ <td>
91
+ <button class="btn btn-sm btn-primary" onclick="editUser('{{ user.username }}', '{{ user.email }}', {{ user.is_admin | tojson }}, {{ user.is_active | tojson }})">Edit</button>
92
+ <form method="post" action="/admin/delete/{{ user.username }}" style="display: inline;">
93
+ <button type="submit" class="btn btn-sm btn-danger">Delete</button>
94
+ </form>
95
+ </td>
96
+ </tr>
97
+ {% endfor %}
98
+ </tbody>
99
+ </table>
100
+ </div>
101
+
102
+ <div class="section">
103
+ <div class="section-header">
104
+ <h2>Devices</h2>
105
+ <button class="btn btn-success" onclick="addDevice()">Add Device</button>
106
+ </div>
107
+ <table class="table">
108
+ <thead>
109
+ <tr>
110
+ <th>Name</th>
111
+ <th>Description</th>
112
+ <th>Device ID</th>
113
+ <th>Password</th>
114
+ <th>Actions</th>
115
+ </tr>
116
+ </thead>
117
+ <tbody>
118
+ {% for device in devices %}
119
+ <tr>
120
+ <td>{{ device.name }}</td>
121
+ <td>{{ device.description }}</td>
122
+ <td>{{ device.device_id }}</td>
123
+ <td>{{ device.password }}</td>
124
+ <td>
125
+ <button class="btn btn-sm btn-primary" onclick="editDevice('{{ device.name }}', '{{ device.description }}', '{{ device.device_id }}', '{{ device.password }}')">Edit</button>
126
+ <form method="post" action="/admin/delete_device/{{ device.device_id }}" style="display: inline;">
127
+ <button type="submit" class="btn btn-sm btn-danger">Delete</button>
128
+ </form>
129
+ </td>
130
+ </tr>
131
+ {% endfor %}
132
+ </tbody>
133
+ </table>
134
+ </div>
135
 
136
+ <div class="section">
137
+ <div class="section-header">
138
+ <h2>System Settings</h2>
139
+ <button class="btn btn-primary" onclick="editSystemSetting(
140
+ {{ system_setting.check_connect_period }},
141
+ {{ system_setting.data_sync_period }},
142
+ {{ system_setting.get_config_period }},
143
+ {{ system_setting.point_distance }}
144
+ )">Edit Settings</button>
145
+ </div>
146
+ <table class="table">
147
+ <thead>
148
+ <tr>
149
+ <th>Setting</th>
150
+ <th>Value</th>
151
+ </tr>
152
+ </thead>
153
+ <tbody>
154
+ <tr>
155
+ <td>Check Connect Period</td>
156
+ <td>{{ system_setting.check_connect_period }}</td>
157
+ </tr>
158
+ <tr>
159
+ <td>Data Sync Period</td>
160
+ <td>{{ system_setting.data_sync_period }}</td>
161
+ </tr>
162
+ <tr>
163
+ <td>Get Config Period</td>
164
+ <td>{{ system_setting.get_config_period }}</td>
165
+ </tr>
166
+ <tr>
167
+ <td>Point Distance</td>
168
+ <td>{{ system_setting.point_distance }}</td>
169
+ </tr>
170
+ </tbody>
171
+ </table>
172
+ </div>
173
 
174
+ <div class="bottom-nav">
175
+ <a href="/" class="btn btn-secondary">Back to Map</a>
176
+ </div>
177
  </div>
178
 
179
  <!-- Edit User Modal -->
 
286
  </div>
287
  </div>
288
 
289
+ <!-- Edit System Setting Modal -->
290
+ <div class="modal fade" id="editSystemSettingModal" tabindex="-1" aria-hidden="true">
291
+ <div class="modal-dialog">
292
+ <div class="modal-content">
293
+ <div class="modal-header">
294
+ <h5 class="modal-title">Edit System Settings</h5>
295
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
296
+ </div>
297
+ <form id="editSystemSettingForm" method="post" action="/admin/edit_system_setting">
298
+ <div class="modal-body">
299
+ <div class="mb-3">
300
+ <label for="checkConnectPeriod" class="form-label">Check Connect Period</label>
301
+ <input type="number" class="form-control" id="checkConnectPeriod" name="check_connect_period" required>
302
+ </div>
303
+ <div class="mb-3">
304
+ <label for="dataSyncPeriod" class="form-label">Data Sync Period</label>
305
+ <input type="number" class="form-control" id="dataSyncPeriod" name="data_sync_period" required>
306
+ </div>
307
+ <div class="mb-3">
308
+ <label for="getConfigPeriod" class="form-label">Get Config Period</label>
309
+ <input type="number" class="form-control" id="getConfigPeriod" name="get_config_period" required>
310
+ </div>
311
+ <div class="mb-3">
312
+ <label for="pointDistance" class="form-label">Point Distance</label>
313
+ <input type="number" class="form-control" id="pointDistance" name="point_distance" required>
314
+ </div>
315
+ </div>
316
+ <div class="modal-footer">
317
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
318
+ <button type="submit" class="btn btn-primary">Save changes</button>
319
+ </div>
320
+ </form>
321
+ </div>
322
+ </div>
323
+ </div>
324
+
325
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
326
  <script>
327
  function editUser(username, email, isAdmin, isActive) {
 
339
  addDeviceModal.show();
340
  }
341
 
342
+ function editDevice(name, description, deviceId, password) {
343
  document.getElementById('editDeviceForm').action = `/admin/edit_device/${deviceId}`;
344
  document.getElementById('editDeviceName').value = name;
345
  document.getElementById('editDeviceDescription').value = description;
346
  document.getElementById('editDeviceId').value = deviceId;
347
+ document.getElementById('editDevicePassword').value = password; // Set the current password
348
  var editDeviceModal = new bootstrap.Modal(document.getElementById('editDeviceModal'));
349
  editDeviceModal.show();
350
  }
351
+
352
+ function editSystemSetting(checkConnectPeriod, dataSyncPeriod, getConfigPeriod, pointDistance) {
353
+ document.getElementById('checkConnectPeriod').value = checkConnectPeriod;
354
+ document.getElementById('dataSyncPeriod').value = dataSyncPeriod;
355
+ document.getElementById('getConfigPeriod').value = getConfigPeriod;
356
+ document.getElementById('pointDistance').value = pointDistance;
357
+ var editSystemSettingModal = new bootstrap.Modal(document.getElementById('editSystemSettingModal'));
358
+ editSystemSettingModal.show();
359
+ }
360
  </script>
361
  </body>
362
  </html>
templates/admin_data.html ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Admin Data - Signal Tracker</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ .user-info {
11
+ position: absolute;
12
+ top: 20px;
13
+ right: 20px;
14
+ background-color: white;
15
+ padding: 10px 15px;
16
+ border-radius: 25px;
17
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
18
+ display: flex;
19
+ align-items: center;
20
+ font-family: Arial, sans-serif;
21
+ font-size: 14px;
22
+ }
23
+ .user-info span {
24
+ font-weight: bold;
25
+ margin-right: 10px;
26
+ }
27
+ .user-info a {
28
+ text-decoration: none;
29
+ color: #007bff;
30
+ padding: 5px 10px;
31
+ border-radius: 15px;
32
+ transition: background-color 0.3s, color 0.3s;
33
+ }
34
+ .user-info a:hover {
35
+ background-color: #007bff;
36
+ color: white;
37
+ }
38
+ .pagination .page-link {
39
+ color: #007bff;
40
+ background-color: #fff;
41
+ border: 1px solid #dee2e6;
42
+ }
43
+ .pagination .page-item.active .page-link {
44
+ color: #fff;
45
+ background-color: #007bff;
46
+ border-color: #007bff;
47
+ }
48
+ .pagination .page-link:hover {
49
+ color: #0056b3;
50
+ background-color: #e9ecef;
51
+ border-color: #dee2e6;
52
+ }
53
+ .pagination .page-item.disabled .page-link {
54
+ color: #6c757d;
55
+ pointer-events: none;
56
+ background-color: #fff;
57
+ border-color: #dee2e6;
58
+ }
59
+ </style>
60
+ </head>
61
+ <body>
62
+ <div class="container mt-5">
63
+ <div class="user-info">
64
+ <span>Welcome, {{ current_user.username }}</span>
65
+ <a href="/logout">Logout</a>
66
+ </div>
67
+
68
+ <h1 class="mb-4">Admin Panel</h1>
69
+
70
+ <ul class="nav nav-tabs mb-4">
71
+ <li class="nav-item">
72
+ <a class="nav-link active" href="/admin/data">Data</a>
73
+ </li>
74
+ <li class="nav-item">
75
+ <a class="nav-link" href="/admin">Settings</a>
76
+ </li>
77
+ </ul>
78
+
79
+ <form method="get" action="/admin/data" class="mb-4">
80
+ <div class="row g-3">
81
+ <div class="col-md-2">
82
+ <label for="start_date" class="form-label">Start Date</label>
83
+ <input type="date" class="form-control" id="start_date" name="start_date" value="{{ start_date }}" placeholder="Start Date">
84
+ </div>
85
+ <div class="col-md-2">
86
+ <label for="end_date" class="form-label">End Date</label>
87
+ <input type="date" class="form-control" id="end_date" name="end_date" value="{{ end_date }}" placeholder="End Date">
88
+ </div>
89
+ <div class="col-md-2">
90
+ <label for="device_id" class="form-label">Device</label>
91
+ <select class="form-control" id="device_id" name="device_id">
92
+ <option value="">All Devices</option>
93
+ {% for dev_id in device_ids %}
94
+ <option value="{{ dev_id }}" {% if dev_id == device_id %}selected{% endif %}>{{ dev_id }}</option>
95
+ {% endfor %}
96
+ </select>
97
+ </div>
98
+ <div class="col-md-2">
99
+ <label for="per_page" class="form-label">Records per page</label>
100
+ <select class="form-control" id="per_page" name="per_page">
101
+ <option value="10" {% if per_page == 10 or not per_page %}selected{% endif %}>10 records</option>
102
+ <option value="50" {% if per_page == 50 %}selected{% endif %}>50 records</option>
103
+ <option value="100" {% if per_page == 100 %}selected{% endif %}>100 records</option>
104
+ </select>
105
+ </div>
106
+ <div class="col-md-2">
107
+ <label class="form-label">&nbsp;</label>
108
+ <button type="submit" class="btn btn-primary w-100">Filter</button>
109
+ </div>
110
+ </div>
111
+ </form>
112
+
113
+ <table class="table">
114
+ <thead>
115
+ <tr>
116
+ <th>Device ID</th>
117
+ <th>Latitude</th>
118
+ <th>Longitude</th>
119
+ <th>Timestamp</th>
120
+ <th>Connect Status</th>
121
+ </tr>
122
+ </thead>
123
+ <tbody>
124
+ {% for record in records %}
125
+ <tr>
126
+ <td>{{ record.device_id }}</td>
127
+ <td>{{ record.latitude }}</td>
128
+ <td>{{ record.longitude }}</td>
129
+ <td>{{ record.timestamp }}</td>
130
+ <td>{{ "Connected" if record.connect_status == 1 else "Disconnected" }}</td>
131
+ </tr>
132
+ {% endfor %}
133
+ </tbody>
134
+ </table>
135
+
136
+ <nav aria-label="Page navigation">
137
+ <ul class="pagination justify-content-center">
138
+ <li class="page-item {% if current_page == 1 %}disabled{% endif %}">
139
+ <a class="page-link" href="/admin/data?page=1&per_page={{ per_page }}{% if start_date %}&start_date={{ start_date }}{% endif %}{% if end_date %}&end_date={{ end_date }}{% endif %}{% if device_id %}&device_id={{ device_id }}{% endif %}" aria-label="First">
140
+ <i class="fas fa-angle-double-left"></i>
141
+ </a>
142
+ </li>
143
+ <li class="page-item {% if current_page == 1 %}disabled{% endif %}">
144
+ <a class="page-link" href="/admin/data?page={{ current_page - 1 }}&per_page={{ per_page }}{% if start_date %}&start_date={{ start_date }}{% endif %}{% if end_date %}&end_date={{ end_date }}{% endif %}{% if device_id %}&device_id={{ device_id }}{% endif %}" aria-label="Previous">
145
+ <i class="fas fa-angle-left"></i>
146
+ </a>
147
+ </li>
148
+
149
+ {% for page_num in range(start_page, end_page + 1) %}
150
+ <li class="page-item {% if page_num == current_page %}active{% endif %}">
151
+ <a class="page-link" href="/admin/data?page={{ page_num }}&per_page={{ per_page }}{% if start_date %}&start_date={{ start_date }}{% endif %}{% if end_date %}&end_date={{ end_date }}{% endif %}{% if device_id %}&device_id={{ device_id }}{% endif %}">{{ page_num }}</a>
152
+ </li>
153
+ {% endfor %}
154
+
155
+ <li class="page-item {% if current_page == total_pages %}disabled{% endif %}">
156
+ <a class="page-link" href="/admin/data?page={{ current_page + 1 }}&per_page={{ per_page }}{% if start_date %}&start_date={{ start_date }}{% endif %}{% if end_date %}&end_date={{ end_date }}{% endif %}{% if device_id %}&device_id={{ device_id }}{% endif %}" aria-label="Next">
157
+ <i class="fas fa-angle-right"></i>
158
+ </a>
159
+ </li>
160
+ <li class="page-item {% if current_page == total_pages %}disabled{% endif %}">
161
+ <a class="page-link" href="/admin/data?page={{ total_pages }}&per_page={{ per_page }}{% if start_date %}&start_date={{ start_date }}{% endif %}{% if end_date %}&end_date={{ end_date }}{% endif %}{% if device_id %}&device_id={{ device_id }}{% endif %}" aria-label="Last">
162
+ <i class="fas fa-angle-double-right"></i>
163
+ </a>
164
+ </li>
165
+ </ul>
166
+ </nav>
167
+
168
+ <div class="mt-4">
169
+ <a href="/" class="btn btn-secondary">Back to Map</a>
170
+ </div>
171
+ </div>
172
+
173
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
174
+ </body>
175
+ </html>
templates/map.html CHANGED
@@ -61,12 +61,35 @@
61
  }
62
  .user-info {
63
  position: absolute;
64
- top: 10px;
65
- right: 10px;
66
  background-color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  padding: 5px 10px;
68
- border-radius: 5px;
69
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
 
 
 
 
 
 
 
 
70
  }
71
  </style>
72
  </head>
@@ -74,7 +97,12 @@
74
  <div class="container">
75
  <div class="user-info">
76
  {% if current_user %}
77
- Welcome, {{ current_user.username }} | <a href="/logout">Logout</a>
 
 
 
 
 
78
  {% else %}
79
  <a href="/login">Login</a>
80
  {% endif %}
 
61
  }
62
  .user-info {
63
  position: absolute;
64
+ top: 20px;
65
+ right: 20px;
66
  background-color: white;
67
+ padding: 10px 15px;
68
+ border-radius: 25px;
69
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
70
+ display: flex;
71
+ align-items: center;
72
+ font-family: Arial, sans-serif;
73
+ font-size: 14px;
74
+ }
75
+ .user-info span {
76
+ font-weight: bold;
77
+ margin-right: 10px;
78
+ }
79
+ .user-info a {
80
+ text-decoration: none;
81
+ color: #007bff;
82
  padding: 5px 10px;
83
+ border-radius: 15px;
84
+ transition: background-color 0.3s, color 0.3s;
85
+ }
86
+ .user-info a:hover {
87
+ background-color: #007bff;
88
+ color: white;
89
+ }
90
+ .user-info .separator {
91
+ margin: 0 5px;
92
+ color: #ccc;
93
  }
94
  </style>
95
  </head>
 
97
  <div class="container">
98
  <div class="user-info">
99
  {% if current_user %}
100
+ <span>Welcome, {{ current_user.username }}</span>
101
+ {% if current_user.is_admin %}
102
+ <a href="/admin">Admin Panel</a>
103
+ <span class="separator">|</span>
104
+ {% endif %}
105
+ <a href="/logout">Logout</a>
106
  {% else %}
107
  <a href="/login">Login</a>
108
  {% endif %}