vumichien commited on
Commit
77d18dc
·
1 Parent(s): 4e2a6d5

upload batch

Browse files
Files changed (2) hide show
  1. app.py +265 -105
  2. models.py +13 -1
app.py CHANGED
@@ -1,5 +1,19 @@
1
- from fastapi import FastAPI, HTTPException, Request, Form, Depends, status, APIRouter, Header
2
- from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse, StreamingResponse
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from fastapi.templating import Jinja2Templates
4
  from fastapi.staticfiles import StaticFiles
5
  from fastapi.security import HTTPBasic
@@ -11,18 +25,21 @@ from folium.plugins import MarkerCluster
11
  from typing import Optional
12
  from sqlalchemy import create_engine
13
  from sqlalchemy.orm import sessionmaker, Session
14
- from models import Base, User, StatusRecord, SystemSetting, Device
15
  import io
16
  import csv
17
  from typing import Dict
18
 
19
  # Database setup
20
  SQLALCHEMY_DATABASE_URL = "sqlite:///./database.db"
21
- engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
 
 
22
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
23
 
24
  Base.metadata.create_all(bind=engine)
25
 
 
26
  # Create default admin user and system settings
27
  def create_default_data():
28
  db = SessionLocal()
@@ -34,7 +51,7 @@ def create_default_data():
34
  email="[email protected]",
35
  password="admin",
36
  is_admin=True,
37
- is_active=True
38
  )
39
  db.add(admin_user)
40
 
@@ -50,6 +67,7 @@ def create_default_data():
50
  finally:
51
  db.close()
52
 
 
53
  create_default_data()
54
 
55
  app = FastAPI()
@@ -60,6 +78,7 @@ security = HTTPBasic()
60
  admin_router = APIRouter(prefix="/admin", tags=["admin"])
61
  api_router = APIRouter(prefix="/api", tags=["api"])
62
 
 
63
  # Dependency to get the database session
64
  def get_db():
65
  db = SessionLocal()
@@ -68,12 +87,14 @@ def get_db():
68
  finally:
69
  db.close()
70
 
 
71
  def get_current_user(request: Request, db: Session = Depends(get_db)) -> Optional[User]:
72
  username = request.cookies.get("username")
73
  if username:
74
  return db.query(User).filter(User.username == username).first()
75
  return None
76
 
 
77
  def login_required(request: Request, db: Session = Depends(get_db)):
78
  username = request.cookies.get("username")
79
  if not username:
@@ -83,27 +104,50 @@ def login_required(request: Request, db: Session = Depends(get_db)):
83
  return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
84
  return user
85
 
 
86
  # Device authentication function
87
- def authenticate_device(device_id: str, device_password: str, db: Session = Depends(get_db)):
 
 
88
  device = db.query(Device).filter(Device.device_id == device_id).first()
89
  if not device or device.password != device_password:
90
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid device credentials")
 
 
 
91
  return device
92
 
 
93
  @app.get("/login", response_class=HTMLResponse)
94
  async def login_page(request: Request):
95
  return templates.TemplateResponse("login.html", {"request": request})
96
 
 
97
  @app.post("/login")
98
- async def login(request: Request, username: str = Form(...), password: str = Form(...), db: Session = Depends(get_db)):
99
- user = db.query(User).filter(User.username == username, User.password == password, User.is_active == True).first()
 
 
 
 
 
 
 
 
 
 
 
100
  if user:
101
  user.last_login = datetime.now()
102
  db.commit()
103
  response = RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
104
  response.set_cookie(key="username", value=username, httponly=True)
105
  return response
106
- return templates.TemplateResponse("login.html", {"request": request, "error": "Invalid credentials or inactive account"})
 
 
 
 
107
 
108
  @app.get("/logout")
109
  async def logout():
@@ -111,20 +155,30 @@ async def logout():
111
  response.delete_cookie("username")
112
  return response
113
 
 
114
  @app.get("/register", response_class=HTMLResponse)
115
  async def register_page(request: Request):
116
  return templates.TemplateResponse("register.html", {"request": request})
117
 
 
118
  @app.post("/register")
119
- async def register(username: str = Form(...), email: str = Form(...), password: str = Form(...), db: Session = Depends(get_db)):
 
 
 
 
 
120
  existing_user = db.query(User).filter(User.username == username).first()
121
  if existing_user:
122
- return RedirectResponse(url="/register?error=1", status_code=status.HTTP_302_FOUND)
 
 
123
  new_user = User(username=username, email=email, password=password)
124
  db.add(new_user)
125
  db.commit()
126
  return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
127
 
 
128
  # Admin routes
129
  @admin_router.get("", response_class=HTMLResponse)
130
  async def admin_page(request: Request, db: Session = Depends(get_db)):
@@ -132,10 +186,15 @@ async def admin_page(request: Request, db: Session = Depends(get_db)):
132
  if isinstance(current_user, RedirectResponse):
133
  return current_user
134
  if not current_user.is_admin:
135
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
 
 
136
  users = db.query(User).all()
137
  devices = db.query(Device).all()
138
- return templates.TemplateResponse("admin.html", {"request": request, "users": users, "devices": devices})
 
 
 
139
 
140
  @admin_router.post("/delete/{username}")
141
  async def delete_user(request: Request, username: str, db: Session = Depends(get_db)):
@@ -143,23 +202,39 @@ async def delete_user(request: Request, username: str, db: Session = Depends(get
143
  if isinstance(current_user, RedirectResponse):
144
  return current_user
145
  if not current_user.is_admin:
146
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
 
 
147
  user = db.query(User).filter(User.username == username).first()
148
  if user:
149
  db.delete(user)
150
  db.commit()
151
  return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
152
 
 
153
  @admin_router.post("/edit/{username}")
154
- async def edit_user(request: Request, username: str, new_username: str = Form(...), email: str = Form(...), is_admin: bool = Form(False), is_active: bool = Form(False), db: Session = Depends(get_db)):
 
 
 
 
 
 
 
 
155
  current_user = login_required(request, db)
156
  if isinstance(current_user, RedirectResponse):
157
  return current_user
158
  if not current_user.is_admin:
159
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
 
 
160
  user = db.query(User).filter(User.username == username).first()
161
  if user:
162
- if new_username != username and db.query(User).filter(User.username == new_username).first():
 
 
 
163
  raise HTTPException(status_code=400, detail="Username already exists")
164
  user.username = new_username
165
  user.email = email
@@ -168,6 +243,7 @@ async def edit_user(request: Request, username: str, new_username: str = Form(..
168
  db.commit()
169
  return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
170
 
 
171
  @admin_router.post("/add_device")
172
  async def add_device(
173
  request: Request,
@@ -175,23 +251,28 @@ async def add_device(
175
  description: str = Form(...),
176
  device_id: str = Form(...),
177
  password: str = Form(...),
178
- db: Session = Depends(get_db)
179
  ):
180
  current_user = login_required(request, db)
181
  if isinstance(current_user, RedirectResponse):
182
  return current_user
183
  if not current_user.is_admin:
184
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
185
-
 
 
186
  existing_device = db.query(Device).filter(Device.device_id == device_id).first()
187
  if existing_device:
188
  raise HTTPException(status_code=400, detail="Device ID already exists")
189
-
190
- new_device = Device(name=name, description=description, device_id=device_id, password=password)
 
 
191
  db.add(new_device)
192
  db.commit()
193
  return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
194
 
 
195
  @admin_router.post("/edit_device/{device_id}")
196
  async def edit_device(
197
  request: Request,
@@ -200,21 +281,26 @@ async def edit_device(
200
  description: str = Form(...),
201
  new_device_id: str = Form(...),
202
  password: str = Form(...),
203
- db: Session = Depends(get_db)
204
  ):
205
  current_user = login_required(request, db)
206
  if isinstance(current_user, RedirectResponse):
207
  return current_user
208
  if not current_user.is_admin:
209
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
210
-
 
 
211
  device = db.query(Device).filter(Device.device_id == device_id).first()
212
  if not device:
213
  raise HTTPException(status_code=404, detail="Device not found")
214
-
215
- if new_device_id != device_id and db.query(Device).filter(Device.device_id == new_device_id).first():
 
 
 
216
  raise HTTPException(status_code=400, detail="New Device ID already exists")
217
-
218
  device.name = name
219
  device.description = description
220
  device.device_id = new_device_id
@@ -222,26 +308,32 @@ async def edit_device(
222
  db.commit()
223
  return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
224
 
 
225
  @admin_router.post("/delete_device/{device_id}")
226
- async def delete_device(request: Request, device_id: str, db: Session = Depends(get_db)):
 
 
227
  current_user = login_required(request, db)
228
  if isinstance(current_user, RedirectResponse):
229
  return current_user
230
  if not current_user.is_admin:
231
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
232
-
 
 
233
  device = db.query(Device).filter(Device.device_id == device_id).first()
234
  if device:
235
  db.delete(device)
236
  db.commit()
237
  return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
238
 
 
239
  # API routes
240
  @api_router.post("/generate-data")
241
  def generate_data(
242
  device_id: str = Header(...),
243
  device_password: str = Header(...),
244
- db: Session = Depends(get_db)
245
  ):
246
  authenticate_device(device_id, device_password, db)
247
  base_latitude = 35.6837
@@ -264,18 +356,19 @@ def generate_data(
264
  latitude=random_latitude,
265
  longitude=random_longitude,
266
  timestamp=random_time,
267
- connect_status=random_connect_status
268
  )
269
  db.add(status_record)
270
 
271
  db.commit()
272
  return {"message": "Demo data generated successfully"}
273
 
 
274
  @api_router.delete("/delete-data", summary="Delete all status records")
275
  def delete_all_data(
276
  device_id: str = Header(...),
277
  device_password: str = Header(...),
278
- db: Session = Depends(get_db)
279
  ):
280
  """
281
  Delete all status records from the database.
@@ -290,12 +383,15 @@ def delete_all_data(
290
  db.rollback()
291
  raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
292
 
293
- @api_router.delete("/delete-data/{device_id}", summary="Delete status records for a specific device")
 
 
 
294
  def delete_device_data(
295
  device_id: str,
296
  auth_device_id: str = Header(...),
297
  device_password: str = Header(...),
298
- db: Session = Depends(get_db)
299
  ):
300
  """
301
  Delete status records for a specific device ID.
@@ -303,7 +399,9 @@ def delete_device_data(
303
  """
304
  authenticate_device(auth_device_id, device_password, db)
305
  try:
306
- deleted_count = db.query(StatusRecord).filter(StatusRecord.device_id == device_id).delete()
 
 
307
  db.commit()
308
  if deleted_count == 0:
309
  return {"message": f"No data found for device ID: {device_id}"}
@@ -312,56 +410,81 @@ def delete_device_data(
312
  db.rollback()
313
  raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
314
 
315
- @api_router.post("/upload")
316
- async def upload_data(
 
 
317
  device_id: str = Header(...),
318
  device_password: str = Header(...),
319
- uuid_str: str = Form(...),
320
- latitude: float = Form(...),
321
- longitude: float = Form(...),
322
- timestamp: str = Form(...),
323
- connect_status: int = Form(...),
324
- db: Session = Depends(get_db)
325
  ):
326
  """
327
- Upload a new status record.
328
- Requires device authentication and a unique UUID.
 
329
  """
330
  authenticate_device(device_id, device_password, db)
331
-
332
- # Validate UUID
333
- try:
334
- uuid_obj = uuid_module.UUID(uuid_str)
335
- except ValueError:
336
- raise HTTPException(status_code=400, detail="Invalid UUID format")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
  try:
339
- timestamp_dt = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S')
340
- status_record = StatusRecord(
341
- uuid=str(uuid_obj),
342
- device_id=device_id,
343
- latitude=latitude,
344
- longitude=longitude,
345
- timestamp=timestamp_dt,
346
- connect_status=connect_status
347
- )
348
- db.add(status_record)
349
  db.commit()
350
- return {"message": "Data uploaded successfully"}
351
- except IntegrityError:
352
- db.rollback()
353
- raise HTTPException(status_code=400, detail="UUID already exists")
354
- except ValueError:
355
- raise HTTPException(status_code=400, detail="Invalid timestamp format. Use 'YYYY-MM-DD HH:MM:SS'")
356
  except Exception as e:
357
  db.rollback()
358
- raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
 
360
  @api_router.get("/health_check", summary="Check if the API is functioning correctly")
361
  def health_check(
362
  device_id: str = Header(...),
363
  device_password: str = Header(...),
364
- db: Session = Depends(get_db)
365
  ):
366
  """
367
  Perform a health check on the API.
@@ -374,14 +497,20 @@ def health_check(
374
  return JSONResponse(content={"status": "ok"}, status_code=status.HTTP_200_OK)
375
  except HTTPException as e:
376
  if e.status_code == status.HTTP_401_UNAUTHORIZED:
377
- return JSONResponse(content={"status": "error", "detail": "Unauthorized"}, status_code=status.HTTP_401_UNAUTHORIZED)
 
 
 
378
  raise e
379
 
380
- @api_router.get("/config", summary="Get system configuration", response_model=Dict[str, int])
 
 
 
381
  def get_config(
382
  device_id: str = Header(...),
383
  device_password: str = Header(...),
384
- db: Session = Depends(get_db)
385
  ):
386
  """
387
  Retrieve the system configuration from SystemSetting.
@@ -391,77 +520,108 @@ def get_config(
391
  system_setting = db.query(SystemSetting).first()
392
  if not system_setting:
393
  raise HTTPException(status_code=404, detail="System settings not found")
394
-
395
  return {
396
  "check_connect_period": system_setting.check_connect_period,
397
  "data_sync_period": system_setting.data_sync_period,
398
  "get_config_period": system_setting.get_config_period,
399
- "point_distance": system_setting.point_distance
400
  }
401
 
 
402
  @app.get("/", response_class=HTMLResponse)
403
- def show_map(request: Request, start_date: str = None, end_date: str = None, db: Session = Depends(get_db)):
 
 
 
 
 
404
  current_user = login_required(request, db)
405
  if isinstance(current_user, RedirectResponse):
406
  return current_user
407
-
408
  query = db.query(StatusRecord)
409
  if start_date and end_date:
410
- start_datetime = datetime.strptime(start_date, '%Y-%m-%d')
411
- end_datetime = datetime.strptime(end_date, '%Y-%m-%d')
412
- query = query.filter(StatusRecord.timestamp.between(start_datetime, end_datetime))
413
-
414
- status_data = [(s.latitude, s.longitude, s.connect_status, s.device_id) for s in query.all()]
 
 
 
 
415
 
416
  m = folium.Map(location=[35.6837, 139.6805], zoom_start=12)
417
  marker_cluster = MarkerCluster().add_to(m)
418
 
419
  for lat, lon, connect_status, device_id in status_data:
420
- color = 'green' if connect_status == 1 else 'red'
421
  folium.CircleMarker(
422
  location=[lat, lon],
423
  radius=10,
424
- popup=f'{device_id}',
425
  color=color,
426
  fill=True,
427
- fill_opacity=0.6
428
  ).add_to(marker_cluster)
429
-
430
  map_html = m._repr_html_()
431
- return templates.TemplateResponse("map.html", {
432
- "request": request,
433
- "map_html": map_html,
434
- "start_date": start_date,
435
- "end_date": end_date,
436
- "current_user": current_user
437
- })
 
 
 
 
438
 
439
  @app.get("/download-csv")
440
  async def download_csv(request: Request, db: Session = Depends(get_db)):
441
  current_user = login_required(request, db)
442
  if isinstance(current_user, RedirectResponse):
443
  return current_user
444
-
445
  status_records = db.query(StatusRecord).all()
446
-
447
  output = io.StringIO()
448
  writer = csv.writer(output)
449
-
450
- writer.writerow(["UUID", "Device ID", "Latitude", "Longitude", "Timestamp", "Connect Status"])
451
-
 
 
452
  for record in status_records:
453
- writer.writerow([record.uuid, record.device_id, record.latitude, record.longitude, record.timestamp, record.connect_status])
454
-
 
 
 
 
 
 
 
 
 
455
  response = StreamingResponse(iter([output.getvalue()]), media_type="text/csv")
456
  response.headers["Content-Disposition"] = "attachment; filename=status_records.csv"
457
-
458
  return response
459
 
 
460
  @app.exception_handler(HTTPException)
461
  async def http_exception_handler(request: Request, exc: HTTPException):
462
  if exc.status_code == status.HTTP_401_UNAUTHORIZED:
463
  return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
464
- return templates.TemplateResponse("error.html", {"request": request, "detail": exc.detail}, status_code=exc.status_code)
 
 
 
 
 
465
 
466
  # Include the routers
467
  app.include_router(admin_router)
@@ -469,5 +629,5 @@ app.include_router(api_router)
469
 
470
  if __name__ == "__main__":
471
  import uvicorn
472
- uvicorn.run(app, host="0.0.0.0", port=7860)
473
 
 
 
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
 
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():
45
  db = SessionLocal()
 
51
  email="[email protected]",
52
  password="admin",
53
  is_admin=True,
54
+ is_active=True,
55
  )
56
  db.add(admin_user)
57
 
 
67
  finally:
68
  db.close()
69
 
70
+
71
  create_default_data()
72
 
73
  app = FastAPI()
 
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()
 
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")
93
  if username:
94
  return db.query(User).filter(User.username == username).first()
95
  return None
96
 
97
+
98
  def login_required(request: Request, db: Session = Depends(get_db)):
99
  username = request.cookies.get("username")
100
  if not username:
 
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)
111
+ ):
112
  device = db.query(Device).filter(Device.device_id == device_id).first()
113
  if not device or device.password != device_password:
114
+ raise HTTPException(
115
+ status_code=status.HTTP_401_UNAUTHORIZED,
116
+ detail="Invalid device credentials",
117
+ )
118
  return device
119
 
120
+
121
  @app.get("/login", response_class=HTMLResponse)
122
  async def login_page(request: Request):
123
  return templates.TemplateResponse("login.html", {"request": request})
124
 
125
+
126
  @app.post("/login")
127
+ async def login(
128
+ request: Request,
129
+ username: str = Form(...),
130
+ password: str = Form(...),
131
+ db: Session = Depends(get_db),
132
+ ):
133
+ user = (
134
+ db.query(User)
135
+ .filter(
136
+ User.username == username, User.password == password, User.is_active == True
137
+ )
138
+ .first()
139
+ )
140
  if user:
141
  user.last_login = datetime.now()
142
  db.commit()
143
  response = RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
144
  response.set_cookie(key="username", value=username, httponly=True)
145
  return response
146
+ return templates.TemplateResponse(
147
+ "login.html",
148
+ {"request": request, "error": "Invalid credentials or inactive account"},
149
+ )
150
+
151
 
152
  @app.get("/logout")
153
  async def logout():
 
155
  response.delete_cookie("username")
156
  return response
157
 
158
+
159
  @app.get("/register", response_class=HTMLResponse)
160
  async def register_page(request: Request):
161
  return templates.TemplateResponse("register.html", {"request": request})
162
 
163
+
164
  @app.post("/register")
165
+ async def register(
166
+ username: str = Form(...),
167
+ email: str = Form(...),
168
+ password: str = Form(...),
169
+ db: Session = Depends(get_db),
170
+ ):
171
  existing_user = db.query(User).filter(User.username == username).first()
172
  if existing_user:
173
+ return RedirectResponse(
174
+ url="/register?error=1", status_code=status.HTTP_302_FOUND
175
+ )
176
  new_user = User(username=username, email=email, password=password)
177
  db.add(new_user)
178
  db.commit()
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)):
 
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)):
 
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
 
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,
 
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,
 
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
 
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
 
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.
 
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.
 
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}"}
 
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
+
432
+ for record in records.records:
433
+ try:
434
+ # Validate UUID
435
+ uuid_obj = uuid_module.UUID(record.uuid)
436
+
437
+ # Validate timestamp
438
+ timestamp_dt = datetime.strptime(record.timestamp, "%Y-%m-%d %H:%M:%S")
439
+
440
+ status_record = StatusRecord(
441
+ uuid=str(uuid_obj),
442
+ device_id=device_id,
443
+ latitude=record.latitude,
444
+ longitude=record.longitude,
445
+ timestamp=timestamp_dt,
446
+ connect_status=record.connect_status,
447
+ )
448
+ db.add(status_record)
449
+ successful_uploads += 1
450
+ except ValueError as ve:
451
+ failed_uploads += 1
452
+ error_messages.append(f"Invalid data format: {str(ve)}")
453
+ except IntegrityError:
454
+ db.rollback()
455
+ failed_uploads += 1
456
+ error_messages.append(f"Duplicate UUID: {record.uuid}")
457
+ except Exception as e:
458
+ db.rollback()
459
+ failed_uploads += 1
460
+ error_messages.append(f"Error processing record: {str(e)}")
461
 
462
  try:
 
 
 
 
 
 
 
 
 
 
463
  db.commit()
 
 
 
 
 
 
464
  except Exception as e:
465
  db.rollback()
466
+ return JSONResponse(
467
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
468
+ content={"message": f"Error committing to database: {str(e)}"},
469
+ )
470
+
471
+ return JSONResponse(
472
+ status_code=status.HTTP_200_OK,
473
+ content={
474
+ "status": "ok",
475
+ "message": "Batch upload completed",
476
+ "successful_uploads": successful_uploads,
477
+ "failed_uploads": failed_uploads,
478
+ "errors": error_messages,
479
+ },
480
+ )
481
+
482
 
483
  @api_router.get("/health_check", summary="Check if the API is functioning correctly")
484
  def health_check(
485
  device_id: str = Header(...),
486
  device_password: str = Header(...),
487
+ db: Session = Depends(get_db),
488
  ):
489
  """
490
  Perform a health check on the API.
 
497
  return JSONResponse(content={"status": "ok"}, status_code=status.HTTP_200_OK)
498
  except HTTPException as e:
499
  if e.status_code == status.HTTP_401_UNAUTHORIZED:
500
+ return JSONResponse(
501
+ content={"status": "error", "detail": "Unauthorized"},
502
+ status_code=status.HTTP_401_UNAUTHORIZED,
503
+ )
504
  raise e
505
 
506
+
507
+ @api_router.get(
508
+ "/config", summary="Get system configuration", response_model=Dict[str, int]
509
+ )
510
  def get_config(
511
  device_id: str = Header(...),
512
  device_password: str = Header(...),
513
+ db: Session = Depends(get_db),
514
  ):
515
  """
516
  Retrieve the system configuration from SystemSetting.
 
520
  system_setting = db.query(SystemSetting).first()
521
  if not system_setting:
522
  raise HTTPException(status_code=404, detail="System settings not found")
523
+
524
  return {
525
  "check_connect_period": system_setting.check_connect_period,
526
  "data_sync_period": system_setting.data_sync_period,
527
  "get_config_period": system_setting.get_config_period,
528
+ "point_distance": system_setting.point_distance,
529
  }
530
 
531
+
532
  @app.get("/", response_class=HTMLResponse)
533
+ def show_map(
534
+ request: Request,
535
+ start_date: str = None,
536
+ end_date: str = None,
537
+ db: Session = Depends(get_db),
538
+ ):
539
  current_user = login_required(request, db)
540
  if isinstance(current_user, RedirectResponse):
541
  return current_user
542
+
543
  query = db.query(StatusRecord)
544
  if start_date and end_date:
545
+ start_datetime = datetime.strptime(start_date, "%Y-%m-%d")
546
+ end_datetime = datetime.strptime(end_date, "%Y-%m-%d")
547
+ query = query.filter(
548
+ StatusRecord.timestamp.between(start_datetime, end_datetime)
549
+ )
550
+
551
+ status_data = [
552
+ (s.latitude, s.longitude, s.connect_status, s.device_id) for s in query.all()
553
+ ]
554
 
555
  m = folium.Map(location=[35.6837, 139.6805], zoom_start=12)
556
  marker_cluster = MarkerCluster().add_to(m)
557
 
558
  for lat, lon, connect_status, device_id in status_data:
559
+ color = "green" if connect_status == 1 else "red"
560
  folium.CircleMarker(
561
  location=[lat, lon],
562
  radius=10,
563
+ popup=f"{device_id}",
564
  color=color,
565
  fill=True,
566
+ fill_opacity=0.6,
567
  ).add_to(marker_cluster)
568
+
569
  map_html = m._repr_html_()
570
+ return templates.TemplateResponse(
571
+ "map.html",
572
+ {
573
+ "request": request,
574
+ "map_html": map_html,
575
+ "start_date": start_date,
576
+ "end_date": end_date,
577
+ "current_user": current_user,
578
+ },
579
+ )
580
+
581
 
582
  @app.get("/download-csv")
583
  async def download_csv(request: Request, db: Session = Depends(get_db)):
584
  current_user = login_required(request, db)
585
  if isinstance(current_user, RedirectResponse):
586
  return current_user
587
+
588
  status_records = db.query(StatusRecord).all()
589
+
590
  output = io.StringIO()
591
  writer = csv.writer(output)
592
+
593
+ writer.writerow(
594
+ ["UUID", "Device ID", "Latitude", "Longitude", "Timestamp", "Connect Status"]
595
+ )
596
+
597
  for record in status_records:
598
+ writer.writerow(
599
+ [
600
+ record.uuid,
601
+ record.device_id,
602
+ record.latitude,
603
+ record.longitude,
604
+ record.timestamp,
605
+ record.connect_status,
606
+ ]
607
+ )
608
+
609
  response = StreamingResponse(iter([output.getvalue()]), media_type="text/csv")
610
  response.headers["Content-Disposition"] = "attachment; filename=status_records.csv"
611
+
612
  return response
613
 
614
+
615
  @app.exception_handler(HTTPException)
616
  async def http_exception_handler(request: Request, exc: HTTPException):
617
  if exc.status_code == status.HTTP_401_UNAUTHORIZED:
618
  return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
619
+ return templates.TemplateResponse(
620
+ "error.html",
621
+ {"request": request, "detail": exc.detail},
622
+ status_code=exc.status_code,
623
+ )
624
+
625
 
626
  # Include the routers
627
  app.include_router(admin_router)
 
629
 
630
  if __name__ == "__main__":
631
  import uvicorn
 
632
 
633
+ uvicorn.run(app, host="0.0.0.0", port=7860)
models.py CHANGED
@@ -2,6 +2,8 @@ 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
 
6
  Base = declarative_base()
7
 
@@ -43,4 +45,14 @@ class Device(Base):
43
  name = Column(String)
44
  description = Column(String)
45
  device_id = Column(String, unique=True, index=True)
46
- password = Column(String)
 
 
 
 
 
 
 
 
 
 
 
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
 
 
45
  name = Column(String)
46
  description = Column(String)
47
  device_id = Column(String, unique=True, index=True)
48
+ password = Column(String)
49
+
50
+ class StatusRecordCreate(BaseModel):
51
+ uuid: str
52
+ latitude: float
53
+ longitude: float
54
+ timestamp: str
55
+ connect_status: int
56
+
57
+ class StatusRecordBatch(BaseModel):
58
+ records: List[StatusRecordCreate]