ChandimaPrabath commited on
Commit
08a1fe6
·
1 Parent(s): 91bab67
Files changed (6) hide show
  1. .gitignore +4 -0
  2. Dockerfile +14 -0
  3. Docs.md +6 -0
  4. main.py +358 -0
  5. requirements.txt +4 -0
  6. run +1 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ sessions_db.json
2
+ users_db.json
3
+ deviceinfo.py
4
+ .env
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.9
3
+
4
+ RUN useradd -m -u 1000 user
5
+ USER user
6
+ ENV PATH="/home/user/.local/bin:$PATH"
7
+
8
+ WORKDIR /app
9
+
10
+ COPY --chown=user ./requirements.txt requirements.txt
11
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
12
+
13
+ COPY --chown=user . /app
14
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
Docs.md ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Access_Levels:
2
+ - Default: Basic access level for regular users. Mostly for the Chat application accounts
3
+ - Member: Standard member privileges
4
+ - Admin: Administrative access
5
+ - Dev: Developer access for technical operations
6
+ - Hush: Special restricted access level
main.py ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import FastAPI, HTTPException, Header, status, APIRouter
3
+ from pydantic import BaseModel, EmailStr
4
+ from typing import Dict, List
5
+ from datetime import datetime, timedelta, timezone
6
+ import secrets
7
+ import threading
8
+ import time
9
+ import json
10
+ import hashlib
11
+ import uuid
12
+ from user_agents import parse
13
+ from dotenv import load_dotenv
14
+
15
+ load_dotenv()
16
+
17
+ SYSTEM_USER = os.getenv("SYSTEM_USER")
18
+ SYSTEM_PASSWORD = os.getenv("SYSTEM_PASSWORD")
19
+
20
+ app = FastAPI()
21
+
22
+ # Routers
23
+ auth_router = APIRouter(prefix="/v1/api/auth", tags=["Authentication"])
24
+ admin_router = APIRouter(prefix="/v1/api/admin", tags=["Admin"])
25
+
26
+ # Simulated in-memory databases
27
+ users_db: Dict[str, dict] = {}
28
+ sessions_db: Dict[str, List[dict]] = {}
29
+
30
+ # Debug Mode
31
+ DEBUG = True
32
+ DB_FILE_USERS = "users_db.json"
33
+ DB_FILE_SESSIONS = "sessions_db.json"
34
+
35
+ if DEBUG:
36
+ try:
37
+ with open(DB_FILE_USERS, "r") as f:
38
+ users_db = json.load(f)
39
+ except FileNotFoundError:
40
+ users_db = {}
41
+
42
+ try:
43
+ with open(DB_FILE_SESSIONS, "r") as f:
44
+ sessions_db = json.load(f)
45
+ except FileNotFoundError:
46
+ sessions_db = {}
47
+
48
+ # Constants
49
+ TOKEN_EXPIRATION_MINUTES = 60
50
+ ACCESS_LEVELS = ["default", "member", "admin", "dev", "hush"]
51
+
52
+ # Models
53
+ class SignupRequest(BaseModel):
54
+ username: str
55
+ password: str
56
+ email: EmailStr = None # Make email optional
57
+
58
+ class LoginRequest(BaseModel):
59
+ username: str
60
+ password: str
61
+
62
+ class TokenResponse(BaseModel):
63
+ access_token: str
64
+ token_type: str = "bearer"
65
+
66
+ class UserResponse(BaseModel):
67
+ username: str
68
+ email: EmailStr
69
+ access_level: str
70
+ date_joined: datetime
71
+
72
+ class UpdateUserRequest(BaseModel):
73
+ password: str = None
74
+ email: EmailStr = None
75
+ username: str = None
76
+
77
+ class UpdateAccessLevelRequest(BaseModel):
78
+ access_level: str
79
+
80
+ # Utility functions
81
+ def hash_password(password: str) -> str:
82
+ return hashlib.sha256(password.encode()).hexdigest()
83
+
84
+ def verify_password(password: str, hashed_password: str) -> bool:
85
+ return hash_password(password) == hashed_password
86
+
87
+ def create_device_token(username: str, user_agent: str) -> str:
88
+ device_info = parse(user_agent)
89
+ token_seed = f"{username}-{device_info.device.family}-{device_info.os.family}-{secrets.token_hex(16)}"
90
+ return hashlib.sha256(token_seed.encode()).hexdigest()
91
+
92
+ def is_token_expired(expiration_time: datetime) -> bool:
93
+ return datetime.now(timezone.utc) > expiration_time
94
+
95
+ def save_databases():
96
+ if DEBUG:
97
+ with open(DB_FILE_USERS, "w") as f:
98
+ json.dump(users_db, f, default=str)
99
+
100
+ with open(DB_FILE_SESSIONS, "w") as f:
101
+ json.dump(sessions_db, f, default=str)
102
+
103
+ # Background task for cleaning up expired sessions
104
+ def cleanup_expired_sessions():
105
+ while True:
106
+ now = datetime.now(timezone.utc)
107
+ for user_id, sessions in list(sessions_db.items()):
108
+ sessions_db[user_id] = [
109
+ session for session in sessions if session["expires"] > now
110
+ ]
111
+ if not sessions_db[user_id]: # Remove user if no active sessions
112
+ del sessions_db[user_id]
113
+ save_databases()
114
+ time.sleep(60) # Run cleanup every 60 seconds
115
+
116
+ # Set timezone
117
+ now = datetime.now(timezone.utc)
118
+ print(f"Server starting at: {now.astimezone(timezone(timedelta(hours=5, minutes=30)))} (Sri Lankan Time)")
119
+
120
+ # Start the background cleanup task
121
+ cleanup_thread = threading.Thread(target=cleanup_expired_sessions, daemon=True)
122
+ cleanup_thread.start()
123
+
124
+ # Create system hush user
125
+ SYSTEM_USER_ID = str(uuid.uuid4())
126
+ users_db[SYSTEM_USER_ID] = {
127
+ "username": SYSTEM_USER,
128
+ "password": hash_password(SYSTEM_PASSWORD),
129
+ "email": None,
130
+ "date_joined": datetime.now(timezone.utc),
131
+ "access_level": "hush",
132
+ }
133
+ save_databases()
134
+
135
+ # Authentication Routes
136
+ @auth_router.post("/signup", status_code=status.HTTP_201_CREATED)
137
+ def signup(request: SignupRequest):
138
+ for user in users_db.values():
139
+ if user["username"] == request.username:
140
+ raise HTTPException(
141
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Username already exists"
142
+ )
143
+
144
+ user_id = str(uuid.uuid4())
145
+ date_joined = datetime.now(timezone.utc)
146
+
147
+ # Auto apply default access level
148
+ users_db[user_id] = {
149
+ "username": request.username,
150
+ "password": hash_password(request.password),
151
+ "email": request.email,
152
+ "date_joined": date_joined,
153
+ "access_level": "default", # Default access level
154
+ }
155
+ save_databases()
156
+ return {"message": "User created successfully"}
157
+
158
+ @auth_router.post("/login", response_model=TokenResponse)
159
+ def login(request: LoginRequest, user_agent: str = Header(...)):
160
+ user_id = next((uid for uid, user in users_db.items() if user["username"] == request.username), None)
161
+ if not user_id or not verify_password(request.password, users_db[user_id]["password"]):
162
+ raise HTTPException(
163
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
164
+ )
165
+
166
+ # Generate a new device-specific session token
167
+ token = create_device_token(request.username, user_agent)
168
+ expiration_time = datetime.now(timezone.utc) + timedelta(minutes=TOKEN_EXPIRATION_MINUTES)
169
+
170
+ # Add the session
171
+ if user_id not in sessions_db:
172
+ sessions_db[user_id] = []
173
+ sessions_db[user_id].append({"token": token, "expires": expiration_time, "device": user_agent})
174
+ save_databases()
175
+
176
+ return TokenResponse(access_token=token)
177
+
178
+ @auth_router.post("/logout")
179
+ def logout(user_id: str, token: str):
180
+ sessions = sessions_db.get(user_id)
181
+ if not sessions:
182
+ raise HTTPException(
183
+ status_code=status.HTTP_400_BAD_REQUEST, detail="No active sessions"
184
+ )
185
+ sessions_db[user_id] = [s for s in sessions if s["token"] != token]
186
+ if not sessions_db[user_id]:
187
+ del sessions_db[user_id]
188
+ save_databases()
189
+ return {"message": "Session forcefully expired"}
190
+
191
+ @auth_router.get("/validate", response_model=TokenResponse)
192
+ def validate_token(user_id: str, token: str, user_agent: str = Header(...)):
193
+ sessions = sessions_db.get(user_id)
194
+ if not sessions:
195
+ raise HTTPException(
196
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="No active sessions"
197
+ )
198
+
199
+ for session in sessions:
200
+ if session["token"] == token:
201
+ if session["device"] != user_agent:
202
+ raise HTTPException(
203
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Device mismatch"
204
+ )
205
+ if is_token_expired(session["expires"]):
206
+ sessions.remove(session)
207
+ if not sessions:
208
+ del sessions_db[user_id]
209
+ save_databases()
210
+ raise HTTPException(
211
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired"
212
+ )
213
+ return TokenResponse(access_token=token)
214
+
215
+ raise HTTPException(
216
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
217
+ )
218
+
219
+ # Admin Routes
220
+ @admin_router.get("/users", response_model=List[UserResponse])
221
+ def get_all_users(user_id: str, token: str, user_agent: str = Header(...)):
222
+ validate_token(user_id, token, user_agent)
223
+
224
+ if users_db[user_id]["access_level"] != "hush":
225
+ raise HTTPException(
226
+ status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
227
+ )
228
+
229
+ return [
230
+ {
231
+ "username": data["username"],
232
+ "email": data["email"],
233
+ "access_level": data["access_level"],
234
+ "date_joined": data["date_joined"],
235
+ }
236
+ for data in users_db.values()
237
+ ]
238
+
239
+ @admin_router.get("/user/{user_id}", response_model=UserResponse)
240
+ def get_user(admin_id: str, token: str, user_id: str, user_agent: str = Header(...)):
241
+ validate_token(admin_id, token, user_agent)
242
+
243
+ if users_db[admin_id]["access_level"] != "hush":
244
+ raise HTTPException(
245
+ status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
246
+ )
247
+
248
+ user = users_db.get(user_id)
249
+ if not user:
250
+ raise HTTPException(
251
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
252
+ )
253
+ return {
254
+ "username": user["username"],
255
+ "email": user["email"],
256
+ "access_level": user["access_level"],
257
+ "date_joined": user["date_joined"],
258
+ }
259
+
260
+ @admin_router.put("/user/{user_id}", response_model=UserResponse)
261
+ def update_user(admin_id: str, token: str, user_id: str, request: UpdateUserRequest, user_agent: str = Header(...)):
262
+ validate_token(admin_id, token, user_agent)
263
+
264
+ if users_db[admin_id]["access_level"] != "hush":
265
+ raise HTTPException(
266
+ status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
267
+ )
268
+
269
+ user = users_db.get(user_id)
270
+ if not user:
271
+ raise HTTPException(
272
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
273
+ )
274
+
275
+ # Update only the fields provided in the request
276
+ if request.password:
277
+ user["password"] = hash_password(request.password)
278
+ if request.email:
279
+ user["email"] = request.email
280
+ if request.username:
281
+ user["username"] = request.username
282
+
283
+ users_db[user_id] = user
284
+ save_databases()
285
+ return {
286
+ "username": user["username"],
287
+ "email": user["email"],
288
+ "access_level": user["access_level"],
289
+ "date_joined": user["date_joined"],
290
+ }
291
+
292
+ @admin_router.put("/user/{user_id}/access-level")
293
+ def update_access_level(admin_id: str, token: str, user_id: str, request: UpdateAccessLevelRequest, user_agent: str = Header(...)):
294
+ validate_token(admin_id, token, user_agent)
295
+
296
+ if users_db[admin_id]["access_level"] != "hush":
297
+ raise HTTPException(
298
+ status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
299
+ )
300
+
301
+ if user_id not in users_db:
302
+ raise HTTPException(
303
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
304
+ )
305
+
306
+ user = users_db.get(user_id)
307
+ new_access_level = request.access_level
308
+
309
+ # Check if the user has the necessary access level to perform the upgrade
310
+ if ACCESS_LEVELS.index(new_access_level) <= ACCESS_LEVELS.index(user["access_level"]):
311
+ raise HTTPException(
312
+ status_code=status.HTTP_400_BAD_REQUEST,
313
+ detail="Cannot downgrade a user or change to the same level",
314
+ )
315
+
316
+ user["access_level"] = new_access_level
317
+ users_db[user_id] = user
318
+ save_databases()
319
+
320
+ return {
321
+ "username": user["username"],
322
+ "email": user["email"],
323
+ "access_level": user["access_level"],
324
+ "date_joined": user["date_joined"],
325
+ }
326
+
327
+ # User Auth Routes
328
+ @auth_router.put("/user/update", response_model=UserResponse)
329
+ def update_own_data(user_id: str, request: UpdateUserRequest, token: str = Header(...), user_agent: str = Header(...)):
330
+ validate_token(user_id, token, user_agent)
331
+
332
+ user = users_db.get(user_id)
333
+ if not user:
334
+ raise HTTPException(
335
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
336
+ )
337
+
338
+ # Update only the fields provided in the request
339
+ if request.password:
340
+ user["password"] = hash_password(request.password)
341
+ if request.email:
342
+ user["email"] = request.email
343
+ if request.username:
344
+ user["username"] = request.username
345
+
346
+ users_db[user_id] = user
347
+ save_databases()
348
+
349
+ return {
350
+ "username": user["username"],
351
+ "email": user["email"],
352
+ "access_level": user["access_level"],
353
+ "date_joined": user["date_joined"],
354
+ }
355
+
356
+ # Include routes
357
+ app.include_router(auth_router)
358
+ app.include_router(admin_router)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi
2
+ pydantic[email]
3
+ user-agents
4
+ python-dotenv
run ADDED
@@ -0,0 +1 @@
 
 
1
+ uvicorn main:app --host 0.0.0.0 --port 8000 --reload