vumichien commited on
Commit
a72a73a
·
1 Parent(s): ab25bb2

Add login required

Browse files
app.py CHANGED
@@ -1,17 +1,191 @@
1
- from fastapi import FastAPI, HTTPException, Request, Form
2
- from fastapi.responses import HTMLResponse, FileResponse
3
  from fastapi.templating import Jinja2Templates
 
 
4
  from datetime import datetime, timedelta
5
  import random
6
  import folium
7
  from folium.plugins import MarkerCluster
8
  import csv
9
  import os
 
 
10
 
11
  app = FastAPI()
12
  templates = Jinja2Templates(directory="templates")
13
 
 
14
  CSV_FILE = "wifi_signals.csv"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  @app.post("/api/generate-data")
17
  def generate_data():
@@ -81,6 +255,10 @@ async def upload_data(
81
 
82
  @app.get("/", response_class=HTMLResponse)
83
  def show_map(request: Request, start_date: str = None, end_date: str = None):
 
 
 
 
84
  signal_data = []
85
 
86
  if os.path.exists(CSV_FILE):
@@ -122,8 +300,19 @@ def show_map(request: Request, start_date: str = None, end_date: str = None):
122
  ).add_to(marker_cluster)
123
 
124
  map_html = m._repr_html_()
125
- return templates.TemplateResponse("map.html", {"request": request, "map_html": map_html, "start_date": start_date, "end_date": end_date})
 
 
 
 
 
 
126
 
 
 
 
 
 
127
 
128
  @app.get("/download-csv")
129
  async def download_csv():
 
1
+ from fastapi import FastAPI, HTTPException, Request, Form, Depends, status
2
+ from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse, JSONResponse
3
  from fastapi.templating import Jinja2Templates
4
+ from fastapi.staticfiles import StaticFiles
5
+ from fastapi.security import HTTPBasic, HTTPBasicCredentials
6
  from datetime import datetime, timedelta
7
  import random
8
  import folium
9
  from folium.plugins import MarkerCluster
10
  import csv
11
  import os
12
+ from pydantic import BaseModel
13
+ from typing import List, Optional
14
 
15
  app = FastAPI()
16
  templates = Jinja2Templates(directory="templates")
17
 
18
+
19
  CSV_FILE = "wifi_signals.csv"
20
+ USERS_FILE = "users.csv"
21
+ security = HTTPBasic()
22
+
23
+ class User(BaseModel):
24
+ username: str
25
+ email: str
26
+ password: str
27
+ is_admin: bool = False
28
+ last_login: Optional[datetime] = None
29
+ is_active: bool = True
30
+
31
+ users = []
32
+
33
+ def load_users():
34
+ global users
35
+ if os.path.exists(USERS_FILE):
36
+ with open(USERS_FILE, mode='r') as file:
37
+ reader = csv.reader(file)
38
+ next(reader) # Skip header
39
+ users = []
40
+ for row in reader:
41
+ user = User(
42
+ username=row[0],
43
+ email=row[1],
44
+ password=row[2],
45
+ is_admin=row[3] == 'True',
46
+ last_login=datetime.fromisoformat(row[4]) if row[4] else None,
47
+ is_active=row[5] == 'True'
48
+ )
49
+ users.append(user)
50
+
51
+ def save_users():
52
+ with open(USERS_FILE, mode='w', newline='') as file:
53
+ writer = csv.writer(file)
54
+ writer.writerow(['username', 'email', 'password', 'is_admin', 'last_login', 'is_active'])
55
+ for user in users:
56
+ writer.writerow([
57
+ user.username,
58
+ user.email,
59
+ user.password,
60
+ user.is_admin,
61
+ user.last_login.isoformat() if user.last_login else '',
62
+ user.is_active
63
+ ])
64
+
65
+ load_users()
66
+
67
+ def get_current_user(request: Request) -> Optional[User]:
68
+ username = request.cookies.get("username")
69
+ if username:
70
+ return next((u for u in users if u.username == username), None)
71
+ return None
72
+
73
+ def login_required(request: Request):
74
+ username = request.cookies.get("username")
75
+ if not username:
76
+ return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
77
+ user = next((u for u in users if u.username == username), None)
78
+ if not user:
79
+ return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
80
+ return user
81
+
82
+ @app.get("/login", response_class=HTMLResponse)
83
+ async def login_page(request: Request):
84
+ return templates.TemplateResponse("login.html", {"request": request})
85
+
86
+ @app.post("/login")
87
+ async def login(request: Request, username: str = Form(...), password: str = Form(...)):
88
+ user = next((u for u in users if u.username == username and u.password == password), None)
89
+ if user and user.is_active:
90
+ user.last_login = datetime.now()
91
+ save_users()
92
+ response = RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
93
+ response.set_cookie(key="username", value=username, httponly=True)
94
+ return response
95
+ return templates.TemplateResponse("login.html", {"request": request, "error": "Invalid credentials or inactive account"})
96
+
97
+ @app.get("/logout")
98
+ async def logout(response: JSONResponse):
99
+ response = RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
100
+ response.delete_cookie("username")
101
+ return response
102
+
103
+ @app.get("/register", response_class=HTMLResponse)
104
+ async def register_page(request: Request):
105
+ return templates.TemplateResponse("register.html", {"request": request})
106
+
107
+ @app.post("/register")
108
+ async def register(username: str = Form(...), email: str = Form(...), password: str = Form(...)):
109
+ if any(u.username == username for u in users):
110
+ return RedirectResponse(url="/register?error=1", status_code=status.HTTP_302_FOUND)
111
+ new_user = User(username=username, email=email, password=password)
112
+ users.append(new_user)
113
+ save_users()
114
+ return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
115
+
116
+ @app.get("/logout")
117
+ async def logout(request: Request):
118
+ response = RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
119
+ response.delete_cookie("username")
120
+ return response
121
+
122
+ @app.get("/admin", response_class=HTMLResponse)
123
+ async def admin_page(request: Request):
124
+ current_user = login_required(request)
125
+ if isinstance(current_user, RedirectResponse):
126
+ return current_user
127
+ if not current_user.is_admin:
128
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
129
+ return templates.TemplateResponse("admin.html", {"request": request, "users": users})
130
+
131
+ @app.post("/admin/delete/{username}")
132
+ async def delete_user(request: Request, username: str):
133
+ current_user = login_required(request)
134
+ if isinstance(current_user, RedirectResponse):
135
+ return current_user
136
+ if not current_user.is_admin:
137
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
138
+ global users
139
+ users = [u for u in users if u.username != username]
140
+ save_users()
141
+ return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
142
+
143
+ @app.post("/admin/edit/{username}")
144
+ 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)):
145
+ current_user = login_required(request)
146
+ if isinstance(current_user, RedirectResponse):
147
+ return current_user
148
+ if not current_user.is_admin:
149
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
150
+ user = next((u for u in users if u.username == username), None)
151
+ if user:
152
+ # Check if the new username already exists
153
+ if new_username != username and any(u.username == new_username for u in users):
154
+ raise HTTPException(status_code=400, detail="Username already exists")
155
+ user.username = new_username
156
+ user.email = email
157
+ user.is_admin = is_admin
158
+ user.is_active = is_active
159
+ save_users()
160
+ return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
161
+
162
+ @app.post("/admin/delete/{username}")
163
+ async def delete_user(username: str, current_user: User = Depends(get_current_user)):
164
+ if not current_user.is_admin:
165
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
166
+ global users
167
+ users = [u for u in users if u.username != username]
168
+ save_users()
169
+ return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
170
+
171
+ @app.post("/admin/edit/{username}")
172
+ 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)):
173
+ current_user = login_required(request)
174
+ if isinstance(current_user, RedirectResponse):
175
+ return current_user
176
+ if not current_user.is_admin:
177
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
178
+ user = next((u for u in users if u.username == username), None)
179
+ if user:
180
+ # Check if the new username already exists
181
+ if new_username != username and any(u.username == new_username for u in users):
182
+ raise HTTPException(status_code=400, detail="Username already exists")
183
+ user.username = new_username
184
+ user.email = email
185
+ user.is_admin = is_admin
186
+ user.is_active = is_active
187
+ save_users()
188
+ return RedirectResponse(url="/admin", status_code=status.HTTP_302_FOUND)
189
 
190
  @app.post("/api/generate-data")
191
  def generate_data():
 
255
 
256
  @app.get("/", response_class=HTMLResponse)
257
  def show_map(request: Request, start_date: str = None, end_date: str = None):
258
+ current_user = login_required(request)
259
+ if isinstance(current_user, RedirectResponse):
260
+ return current_user
261
+
262
  signal_data = []
263
 
264
  if os.path.exists(CSV_FILE):
 
300
  ).add_to(marker_cluster)
301
 
302
  map_html = m._repr_html_()
303
+ return templates.TemplateResponse("map.html", {
304
+ "request": request,
305
+ "map_html": map_html,
306
+ "start_date": start_date,
307
+ "end_date": end_date,
308
+ "current_user": current_user
309
+ })
310
 
311
+ @app.exception_handler(HTTPException)
312
+ async def http_exception_handler(request: Request, exc: HTTPException):
313
+ if exc.status_code == status.HTTP_401_UNAUTHORIZED:
314
+ return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
315
+ return templates.TemplateResponse("error.html", {"request": request, "detail": exc.detail}, status_code=exc.status_code)
316
 
317
  @app.get("/download-csv")
318
  async def download_csv():
templates/admin.html ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 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
+ <a href="/" class="btn btn-secondary">Back to Map</a>
42
+ </div>
43
+
44
+ <!-- Edit User Modal -->
45
+ <div class="modal fade" id="editUserModal" tabindex="-1" aria-hidden="true">
46
+ <div class="modal-dialog">
47
+ <div class="modal-content">
48
+ <div class="modal-header">
49
+ <h5 class="modal-title">Edit User</h5>
50
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
51
+ </div>
52
+ <form id="editUserForm" method="post">
53
+ <div class="modal-body">
54
+ <div class="mb-3">
55
+ <label for="editUsername" class="form-label">Username</label>
56
+ <input type="text" class="form-control" id="editUsername" name="new_username" required>
57
+ </div>
58
+ <div class="mb-3">
59
+ <label for="editEmail" class="form-label">Email</label>
60
+ <input type="email" class="form-control" id="editEmail" name="email" required>
61
+ </div>
62
+ <div class="mb-3 form-check">
63
+ <input type="hidden" name="is_admin" value="false">
64
+ <input type="checkbox" class="form-check-input" id="editIsAdmin" name="is_admin" value="true">
65
+ <label class="form-check-label" for="editIsAdmin">Is Admin</label>
66
+ </div>
67
+ <div class="mb-3 form-check">
68
+ <input type="hidden" name="is_active" value="false">
69
+ <input type="checkbox" class="form-check-input" id="editIsActive" name="is_active" value="true">
70
+ <label class="form-check-label" for="editIsActive">Is Active</label>
71
+ </div>
72
+ </div>
73
+ <div class="modal-footer">
74
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
75
+ <button type="submit" class="btn btn-primary">Save changes</button>
76
+ </div>
77
+ </form>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
83
+ <script>
84
+ function editUser(username, email, isAdmin, isActive) {
85
+ document.getElementById('editUserForm').action = `/admin/edit/${username}`;
86
+ document.getElementById('editUsername').value = username;
87
+ document.getElementById('editEmail').value = email;
88
+ document.getElementById('editIsAdmin').checked = isAdmin;
89
+ document.getElementById('editIsActive').checked = isActive;
90
+ var editUserModal = new bootstrap.Modal(document.getElementById('editUserModal'));
91
+ editUserModal.show();
92
+ }
93
+ </script>
94
+ </body>
95
+ </html>
96
+
97
+
templates/login.html ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Login - 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>Login</h1>
12
+ {% if request.query_params.get("error") %}
13
+ <div class="alert alert-danger">Invalid credentials</div>
14
+ {% endif %}
15
+ <form method="post" action="/login">
16
+ <div class="mb-3">
17
+ <label for="username" class="form-label">Username</label>
18
+ <input type="text" class="form-control" id="username" name="username" required>
19
+ </div>
20
+ <div class="mb-3">
21
+ <label for="password" class="form-label">Password</label>
22
+ <input type="password" class="form-control" id="password" name="password" required>
23
+ </div>
24
+ <button type="submit" class="btn btn-primary">Login</button>
25
+ </form>
26
+ <p class="mt-3">Don't have an account? <a href="/register">Register here</a></p>
27
+ </div>
28
+ </body>
29
+ </html>
templates/map.html CHANGED
@@ -59,10 +59,26 @@
59
  .btn-download:hover {
60
  background-color: #0056b3;
61
  }
 
 
 
 
 
 
 
 
 
62
  </style>
63
  </head>
64
  <body>
65
  <div class="container">
 
 
 
 
 
 
 
66
  <h1 class="title">Signal Tracker Map</h1>
67
  <form class="filter-form" id="date-form" method="GET" action="/">
68
  <div class="row g-3 align-items-center">
 
59
  .btn-download:hover {
60
  background-color: #0056b3;
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>
73
  <body>
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 %}
81
+ </div>
82
  <h1 class="title">Signal Tracker Map</h1>
83
  <form class="filter-form" id="date-form" method="GET" action="/">
84
  <div class="row g-3 align-items-center">
templates/register.html ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Register - 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>Register</h1>
12
+ {% if request.query_params.get("error") %}
13
+ <div class="alert alert-danger">Username already exists</div>
14
+ {% endif %}
15
+ <form method="post" action="/register">
16
+ <div class="mb-3">
17
+ <label for="username" class="form-label">Username</label>
18
+ <input type="text" class="form-control" id="username" name="username" required>
19
+ </div>
20
+ <div class="mb-3">
21
+ <label for="email" class="form-label">Email</label>
22
+ <input type="email" class="form-control" id="email" name="email" required>
23
+ </div>
24
+ <div class="mb-3">
25
+ <label for="password" class="form-label">Password</label>
26
+ <input type="password" class="form-control" id="password" name="password" required>
27
+ </div>
28
+ <button type="submit" class="btn btn-primary">Register</button>
29
+ </form>
30
+ <p class="mt-3">Already have an account? <a href="/login">Login here</a></p>
31
+ </div>
32
+ </body>
33
+ </html>
users.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ username,email,password,is_admin,last_login,is_active
2
+ admin,[email protected],admin,True,2024-08-30T17:44:00.209768,True
3
+ vumichien,[email protected],123456,True,2024-08-30T17:43:53.120071,True