ChandimaPrabath commited on
Commit
836f140
·
1 Parent(s): 9381a7e
Files changed (5) hide show
  1. Docs.md +274 -6
  2. __pycache__/main.cpython-313.pyc +0 -0
  3. init_supabase_sql.txt +47 -0
  4. main.py +177 -209
  5. requirements.txt +2 -1
Docs.md CHANGED
@@ -1,6 +1,274 @@
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Authentication System API Documentation
2
+
3
+ ## Base URL
4
+ ```
5
+ /v1/api
6
+ ```
7
+
8
+ ## Authentication Routes
9
+
10
+ ### 1. Sign Up
11
+ Creates a new user account.
12
+
13
+ **Endpoint:** `POST /auth/signup`
14
+
15
+ **Request Body:**
16
+ ```json
17
+ {
18
+ "username": "string",
19
+ "password": "string",
20
+ "email": "string (optional)"
21
+ }
22
+ ```
23
+
24
+ **Response (201 Created):**
25
+ ```json
26
+ {
27
+ "message": "User created successfully"
28
+ }
29
+ ```
30
+
31
+ **Possible Errors:**
32
+ - `400 Bad Request`: Username already exists
33
+
34
+ ### 2. Login
35
+ Authenticates a user and creates a new session.
36
+
37
+ **Endpoint:** `POST /auth/login`
38
+
39
+ **Headers:**
40
+ - `user-agent`: Browser/device user agent string (required)
41
+
42
+ **Request Body:**
43
+ ```json
44
+ {
45
+ "username": "string",
46
+ "password": "string"
47
+ }
48
+ ```
49
+
50
+ **Response (200 OK):**
51
+ ```json
52
+ {
53
+ "user_id": "string",
54
+ "username": "string",
55
+ "email": "string",
56
+ "access_level": "string",
57
+ "date_joined": "datetime",
58
+ "access_token": "string",
59
+ "token_type": "bearer"
60
+ }
61
+ ```
62
+
63
+ **Possible Errors:**
64
+ - `401 Unauthorized`: Invalid credentials
65
+
66
+ ### 3. Logout
67
+ Terminates an active session.
68
+
69
+ **Endpoint:** `POST /auth/logout`
70
+
71
+ **Query Parameters:**
72
+ - `user_id`: string
73
+ - `token`: string
74
+
75
+ **Response (200 OK):**
76
+ ```json
77
+ {
78
+ "message": "Session forcefully expired"
79
+ }
80
+ ```
81
+
82
+ **Possible Errors:**
83
+ - `400 Bad Request`: No active sessions
84
+
85
+ ### 4. Validate Token
86
+ Validates an existing session token.
87
+
88
+ **Endpoint:** `GET /auth/validate`
89
+
90
+ **Headers:**
91
+ - `user-agent`: Browser/device user agent string (required)
92
+
93
+ **Query Parameters:**
94
+ - `user_id`: string
95
+ - `token`: string
96
+
97
+ **Response (200 OK):**
98
+ ```json
99
+ {
100
+ "access_token": "string",
101
+ "token_type": "bearer"
102
+ }
103
+ ```
104
+
105
+ **Possible Errors:**
106
+ - `401 Unauthorized`: No active sessions, Device mismatch, Token expired, Invalid token
107
+
108
+ ### 5. Search Users
109
+ Search for users by username.
110
+
111
+ **Endpoint:** `GET /auth/search-users`
112
+
113
+ **Query Parameters:**
114
+ - `query`: string
115
+
116
+ **Response (200 OK):**
117
+ ```json
118
+ [
119
+ "string"
120
+ ]
121
+ ```
122
+
123
+ ### 6. Get User ID
124
+ Retrieve user ID by username.
125
+
126
+ **Endpoint:** `GET /auth/get-user-id`
127
+
128
+ **Query Parameters:**
129
+ - `username`: string
130
+
131
+ **Response (200 OK):**
132
+ ```
133
+ "string" (user_id)
134
+ ```
135
+
136
+ **Possible Errors:**
137
+ - `404 Not Found`: Username not found
138
+
139
+ ### 7. Update Own Data
140
+ Update authenticated user's information.
141
+
142
+ **Endpoint:** `PUT /auth/user/update`
143
+
144
+ **Headers:**
145
+ - `token`: string
146
+ - `user-agent`: Browser/device user agent string (required)
147
+
148
+ **Query Parameters:**
149
+ - `user_id`: string
150
+
151
+ **Request Body:**
152
+ ```json
153
+ {
154
+ "password": "string (optional)",
155
+ "email": "string (optional)",
156
+ "username": "string (optional)"
157
+ }
158
+ ```
159
+
160
+ **Response (200 OK):**
161
+ ```json
162
+ {
163
+ "username": "string",
164
+ "email": "string",
165
+ "access_level": "string",
166
+ "date_joined": "datetime"
167
+ }
168
+ ```
169
+
170
+ ## Admin Routes
171
+
172
+ ### 1. Get All Users
173
+ Retrieve all users (requires HUSH access level).
174
+
175
+ **Endpoint:** `GET /admin/users`
176
+
177
+ **Headers:**
178
+ - `user-agent`: Browser/device user agent string (required)
179
+
180
+ **Query Parameters:**
181
+ - `user_id`: string (admin's user ID)
182
+ - `token`: string
183
+
184
+ **Response (200 OK):**
185
+ ```json
186
+ [
187
+ {
188
+ "username": "string",
189
+ "email": "string",
190
+ "access_level": "string",
191
+ "date_joined": "datetime"
192
+ }
193
+ ]
194
+ ```
195
+
196
+ **Possible Errors:**
197
+ - `403 Forbidden`: Insufficient permissions
198
+
199
+ ### 2. Get User Details
200
+ Retrieve specific user details (requires HUSH access level).
201
+
202
+ **Endpoint:** `GET /admin/user/{user_id}`
203
+
204
+ **Headers:**
205
+ - `user-agent`: Browser/device user agent string (required)
206
+
207
+ **Query Parameters:**
208
+ - `admin_id`: string
209
+ - `token`: string
210
+
211
+ **Response (200 OK):**
212
+ ```json
213
+ {
214
+ "username": "string",
215
+ "email": "string",
216
+ "access_level": "string",
217
+ "date_joined": "datetime"
218
+ }
219
+ ```
220
+
221
+ ### 3. Update User
222
+ Update user information (requires HUSH access level).
223
+
224
+ **Endpoint:** `PUT /admin/user/{user_id}`
225
+
226
+ **Headers:**
227
+ - `user-agent`: Browser/device user agent string (required)
228
+
229
+ **Query Parameters:**
230
+ - `admin_id`: string
231
+ - `token`: string
232
+
233
+ **Request Body:**
234
+ ```json
235
+ {
236
+ "password": "string (optional)",
237
+ "email": "string (optional)",
238
+ "username": "string (optional)"
239
+ }
240
+ ```
241
+
242
+ ### 4. Update Access Level
243
+ Update user access level (requires HUSH access level).
244
+
245
+ **Endpoint:** `PUT /admin/user/{user_id}/access-level`
246
+
247
+ **Headers:**
248
+ - `user-agent`: Browser/device user agent string (required)
249
+
250
+ **Query Parameters:**
251
+ - `admin_id`: string
252
+ - `token`: string
253
+
254
+ **Request Body:**
255
+ ```json
256
+ {
257
+ "access_level": "string"
258
+ }
259
+ ```
260
+
261
+ ## Access Levels
262
+ The system supports the following access levels in ascending order of privileges:
263
+ 1. `default`
264
+ 2. `member`
265
+ 3. `admin`
266
+ 4. `dev`
267
+ 5. `hush`
268
+
269
+ ## Notes
270
+ - All timestamps are in UTC
271
+ - Token expiration is set to 60 minutes
272
+ - Sessions are device-specific and validated against the user agent
273
+ - Database changes are saved to disk in debug mode
274
+ - The system automatically creates a HUSH-level system user on startup
__pycache__/main.cpython-313.pyc ADDED
Binary file (20.1 kB). View file
 
init_supabase_sql.txt ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Enable UUID extension
2
+ create extension if not exists "uuid-ossp";
3
+
4
+ -- Users table
5
+ create table if not exists public.users (
6
+ id uuid primary key,
7
+ username text unique not null,
8
+ password text not null,
9
+ email text,
10
+ date_joined timestamp with time zone not null,
11
+ access_level text not null
12
+ );
13
+
14
+ -- Sessions table
15
+ create table if not exists public.sessions (
16
+ id uuid primary key default uuid_generate_v4(),
17
+ user_id uuid references public.users(id),
18
+ token text not null,
19
+ expires timestamp with time zone not null,
20
+ device text not null
21
+ );
22
+
23
+ -- Create indexes for better performance
24
+ create index if not exists idx_users_username on public.users(username);
25
+ create index if not exists idx_sessions_user_id on public.sessions(user_id);
26
+ create index if not exists idx_sessions_token on public.sessions(token);
27
+
28
+ -- Set up Row Level Security (RLS)
29
+ alter table public.users enable row level security;
30
+ alter table public.sessions enable row level security;
31
+
32
+ -- Create policies
33
+ create policy "Enable read access for all users"
34
+ on public.users for select
35
+ using (true);
36
+
37
+ create policy "Enable insert for authenticated users only"
38
+ on public.users for insert
39
+ with check (true);
40
+
41
+ create policy "Enable update for authenticated users"
42
+ on public.users for update
43
+ using (true);
44
+
45
+ create policy "Enable all access for sessions"
46
+ on public.sessions for all
47
+ using (true);
main.py CHANGED
@@ -4,16 +4,20 @@ 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
 
@@ -23,28 +27,6 @@ app = FastAPI()
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"]
@@ -53,7 +35,7 @@ ACCESS_LEVELS = ["default", "member", "admin", "dev", "hush"]
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
@@ -79,9 +61,9 @@ class UserResponse(BaseModel):
79
  date_joined: datetime
80
 
81
  class UpdateUserRequest(BaseModel):
82
- password: str = None
83
- email: EmailStr = None
84
- username: str = None
85
 
86
  class UpdateAccessLevelRequest(BaseModel):
87
  access_level: str
@@ -101,279 +83,265 @@ def create_device_token(username: str, user_agent: str) -> str:
101
  def is_token_expired(expiration_time: datetime) -> bool:
102
  return datetime.now(timezone.utc) > expiration_time
103
 
104
- def save_databases():
105
- if DEBUG:
106
- with open(DB_FILE_USERS, "w") as f:
107
- json.dump(users_db, f, default=str)
108
-
109
- with open(DB_FILE_SESSIONS, "w") as f:
110
- json.dump(sessions_db, f, default=str)
111
-
112
- # Background task for cleaning up expired sessions
113
- def cleanup_expired_sessions():
114
- while True:
115
- now = datetime.now(timezone.utc)
116
- for user_id, sessions in list(sessions_db.items()):
117
- sessions_db[user_id] = [
118
- session for session in sessions if session["expires"] > now
119
- ]
120
- if not sessions_db[user_id]: # Remove user if no active sessions
121
- del sessions_db[user_id]
122
- save_databases()
123
- time.sleep(60) # Run cleanup every 60 seconds
124
-
125
- # Set timezone
126
- now = datetime.now(timezone.utc)
127
- print(f"Server starting at: {now.astimezone(timezone(timedelta(hours=5, minutes=30)))} (Sri Lankan Time)")
128
-
129
- # Start the background cleanup task
130
- cleanup_thread = threading.Thread(target=cleanup_expired_sessions, daemon=True)
131
- cleanup_thread.start()
132
-
133
- # Create system hush user
134
- SYSTEM_USER_ID = str(uuid.uuid4())
135
- users_db[SYSTEM_USER_ID] = {
136
- "username": SYSTEM_USER,
137
- "password": hash_password(SYSTEM_PASSWORD),
138
- "email": None,
139
- "date_joined": datetime.now(timezone.utc),
140
- "access_level": "hush",
141
- }
142
- save_databases()
143
 
144
  # Authentication Routes
145
  @auth_router.post("/signup", status_code=status.HTTP_201_CREATED)
146
- def signup(request: SignupRequest):
147
- for user in users_db.values():
148
- if user["username"] == request.username:
149
- raise HTTPException(
150
- status_code=status.HTTP_400_BAD_REQUEST, detail="Username already exists"
151
- )
152
-
153
- user_id = str(uuid.uuid4())
154
- date_joined = datetime.now(timezone.utc)
155
-
156
- # Auto apply default access level
157
- users_db[user_id] = {
158
  "username": request.username,
159
  "password": hash_password(request.password),
160
  "email": request.email,
161
- "date_joined": date_joined,
162
- "access_level": "default", # Default access level
163
  }
164
- save_databases()
 
165
  return {"message": "User created successfully"}
166
 
167
  @auth_router.post("/login", response_model=LoginResponse)
168
- def login(request: LoginRequest, user_agent: str = Header(...)):
169
- user_id = next((uid for uid, user in users_db.items() if user["username"] == request.username), None)
170
- if not user_id or not verify_password(request.password, users_db[user_id]["password"]):
 
171
  raise HTTPException(
172
  status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
173
  )
174
 
175
- # Generate a new device-specific session token
176
  token = create_device_token(request.username, user_agent)
177
  expiration_time = datetime.now(timezone.utc) + timedelta(minutes=TOKEN_EXPIRATION_MINUTES)
178
 
179
- # Add the session
180
- if user_id not in sessions_db:
181
- sessions_db[user_id] = []
182
- sessions_db[user_id].append({"token": token, "expires": expiration_time, "device": user_agent})
183
- save_databases()
 
 
 
184
 
185
- user = users_db[user_id]
186
  return LoginResponse(
187
- user_id=user_id,
188
  username=user["username"],
189
  email=user["email"],
190
  access_level=user["access_level"],
191
- date_joined=user["date_joined"],
192
  access_token=token
193
  )
194
 
195
  @auth_router.post("/logout")
196
- def logout(user_id: str, token: str):
197
- sessions = sessions_db.get(user_id)
198
- if not sessions:
199
- raise HTTPException(
200
- status_code=status.HTTP_400_BAD_REQUEST, detail="No active sessions"
201
- )
202
- sessions_db[user_id] = [s for s in sessions if s["token"] != token]
203
- if not sessions_db[user_id]:
204
- del sessions_db[user_id]
205
- save_databases()
206
  return {"message": "Session forcefully expired"}
207
 
208
  @auth_router.get("/validate", response_model=TokenResponse)
209
- def validate_token(user_id: str, token: str, user_agent: str = Header(...)):
210
- sessions = sessions_db.get(user_id)
211
- if not sessions:
 
 
 
 
 
 
 
 
212
  raise HTTPException(
213
- status_code=status.HTTP_401_UNAUTHORIZED, detail="No active sessions"
214
  )
215
 
216
- for session in sessions:
217
- if session["token"] == token:
218
- if session["device"] != user_agent:
219
- raise HTTPException(
220
- status_code=status.HTTP_401_UNAUTHORIZED, detail="Device mismatch"
221
- )
222
- if is_token_expired(session["expires"]):
223
- sessions.remove(session)
224
- if not sessions:
225
- del sessions_db[user_id]
226
- save_databases()
227
- raise HTTPException(
228
- status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired"
229
- )
230
- return TokenResponse(access_token=token)
231
-
232
- raise HTTPException(
233
- status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
234
- )
235
 
236
  @auth_router.get("/search-users", response_model=List[str])
237
- def search_users(query: str):
238
- return [user["username"] for user in users_db.values() if query.lower() in user["username"].lower()]
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  # Admin Routes
241
  @admin_router.get("/users", response_model=List[UserResponse])
242
- def get_all_users(user_id: str, token: str, user_agent: str = Header(...)):
243
- validate_token(user_id, token, user_agent)
244
 
245
- if users_db[user_id]["access_level"] != "hush":
 
246
  raise HTTPException(
247
  status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
248
  )
249
 
 
250
  return [
251
- {
252
- "username": data["username"],
253
- "email": data["email"],
254
- "access_level": data["access_level"],
255
- "date_joined": data["date_joined"],
256
- }
257
- for data in users_db.values()
258
  ]
259
 
260
  @admin_router.get("/user/{user_id}", response_model=UserResponse)
261
- def get_user(admin_id: str, token: str, user_id: str, 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
- return {
275
- "username": user["username"],
276
- "email": user["email"],
277
- "access_level": user["access_level"],
278
- "date_joined": user["date_joined"],
279
- }
 
 
280
 
281
  @admin_router.put("/user/{user_id}", response_model=UserResponse)
282
- def update_user(admin_id: str, token: str, user_id: str, request: UpdateUserRequest, user_agent: str = Header(...)):
283
- validate_token(admin_id, token, user_agent)
284
 
285
- if users_db[admin_id]["access_level"] != "hush":
 
286
  raise HTTPException(
287
  status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
288
  )
289
 
290
- user = users_db.get(user_id)
291
- if not user:
 
 
 
 
 
 
 
 
292
  raise HTTPException(
293
  status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
294
  )
295
 
296
- # Update only the fields provided in the request
297
- if request.password:
298
- user["password"] = hash_password(request.password)
299
- if request.email:
300
- user["email"] = request.email
301
- if request.username:
302
- user["username"] = request.username
303
-
304
- users_db[user_id] = user
305
- save_databases()
306
- return {
307
- "username": user["username"],
308
- "email": user["email"],
309
- "access_level": user["access_level"],
310
- "date_joined": user["date_joined"],
311
- }
312
 
313
  @admin_router.put("/user/{user_id}/access-level")
314
- def update_access_level(admin_id: str, token: str, user_id: str, request: UpdateAccessLevelRequest, user_agent: str = Header(...)):
315
- validate_token(admin_id, token, user_agent)
316
 
317
- if users_db[admin_id]["access_level"] != "hush":
 
318
  raise HTTPException(
319
  status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
320
  )
321
 
322
- if user_id not in users_db:
 
323
  raise HTTPException(
324
  status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
325
  )
326
 
327
- user = users_db.get(user_id)
328
  new_access_level = request.access_level
329
 
330
- # Check if the user has the necessary access level to perform the upgrade
331
  if ACCESS_LEVELS.index(new_access_level) <= ACCESS_LEVELS.index(user["access_level"]):
332
  raise HTTPException(
333
  status_code=status.HTTP_400_BAD_REQUEST,
334
  detail="Cannot downgrade a user or change to the same level",
335
  )
336
 
337
- user["access_level"] = new_access_level
338
- users_db[user_id] = user
339
- save_databases()
340
-
341
- return {
342
- "username": user["username"],
343
- "email": user["email"],
344
- "access_level": user["access_level"],
345
- "date_joined": user["date_joined"],
346
- }
347
 
348
- # User Auth Routes
349
  @auth_router.put("/user/update", response_model=UserResponse)
350
- def update_own_data(user_id: str, request: UpdateUserRequest, token: str = Header(...), user_agent: str = Header(...)):
351
- validate_token(user_id, token, user_agent)
352
 
353
- user = users_db.get(user_id)
354
- if not user:
355
- raise HTTPException(
356
- status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
357
- )
358
-
359
- # Update only the fields provided in the request
360
  if request.password:
361
- user["password"] = hash_password(request.password)
362
  if request.email:
363
- user["email"] = request.email
364
  if request.username:
365
- user["username"] = request.username
366
 
367
- users_db[user_id] = user
368
- save_databases()
 
 
 
369
 
370
- return {
371
- "username": user["username"],
372
- "email": user["email"],
373
- "access_level": user["access_level"],
374
- "date_joined": user["date_joined"],
375
- }
 
376
 
377
  # Include routes
378
  app.include_router(auth_router)
379
  app.include_router(admin_router)
 
 
 
 
 
 
4
  from typing import Dict, List
5
  from datetime import datetime, timedelta, timezone
6
  import secrets
 
 
 
7
  import hashlib
8
  import uuid
9
  from user_agents import parse
10
  from dotenv import load_dotenv
11
+ from supabase import create_client, Client
12
+ from typing import Optional
13
 
14
  load_dotenv()
15
 
16
+ # Supabase Configuration
17
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
18
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
19
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
20
+
21
  SYSTEM_USER = os.getenv("SYSTEM_USER")
22
  SYSTEM_PASSWORD = os.getenv("SYSTEM_PASSWORD")
23
 
 
27
  auth_router = APIRouter(prefix="/v1/api/auth", tags=["Authentication"])
28
  admin_router = APIRouter(prefix="/v1/api/admin", tags=["Admin"])
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  # Constants
31
  TOKEN_EXPIRATION_MINUTES = 60
32
  ACCESS_LEVELS = ["default", "member", "admin", "dev", "hush"]
 
35
  class SignupRequest(BaseModel):
36
  username: str
37
  password: str
38
+ email: Optional[EmailStr] = None
39
 
40
  class LoginRequest(BaseModel):
41
  username: str
 
61
  date_joined: datetime
62
 
63
  class UpdateUserRequest(BaseModel):
64
+ password: Optional[str] = None
65
+ email: Optional[EmailStr] = None
66
+ username: Optional[str] = None
67
 
68
  class UpdateAccessLevelRequest(BaseModel):
69
  access_level: str
 
83
  def is_token_expired(expiration_time: datetime) -> bool:
84
  return datetime.now(timezone.utc) > expiration_time
85
 
86
+ # Initialize system user
87
+ async def init_system_user():
88
+ system_user_data = {
89
+ "id": str(uuid.uuid4()),
90
+ "username": SYSTEM_USER,
91
+ "password": hash_password(SYSTEM_PASSWORD),
92
+ "email": None,
93
+ "date_joined": datetime.now(timezone.utc).isoformat(),
94
+ "access_level": "hush"
95
+ }
96
+
97
+ # Check if system user exists
98
+ existing_user = supabase.table("users").select("*").eq("username", SYSTEM_USER).execute()
99
+ if not existing_user.data:
100
+ supabase.table("users").insert(system_user_data).execute()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  # Authentication Routes
103
  @auth_router.post("/signup", status_code=status.HTTP_201_CREATED)
104
+ async def signup(request: SignupRequest):
105
+ # Check if username exists
106
+ existing_user = supabase.table("users").select("*").eq("username", request.username).execute()
107
+ if existing_user.data:
108
+ raise HTTPException(
109
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Username already exists"
110
+ )
111
+
112
+ user_data = {
113
+ "id": str(uuid.uuid4()),
 
 
114
  "username": request.username,
115
  "password": hash_password(request.password),
116
  "email": request.email,
117
+ "date_joined": datetime.now(timezone.utc).isoformat(),
118
+ "access_level": "default"
119
  }
120
+
121
+ supabase.table("users").insert(user_data).execute()
122
  return {"message": "User created successfully"}
123
 
124
  @auth_router.post("/login", response_model=LoginResponse)
125
+ async def login(request: LoginRequest, user_agent: str = Header(...)):
126
+ user_query = supabase.table("users").select("*").eq("username", request.username).execute()
127
+
128
+ if not user_query.data or not verify_password(request.password, user_query.data[0]["password"]):
129
  raise HTTPException(
130
  status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
131
  )
132
 
133
+ user = user_query.data[0]
134
  token = create_device_token(request.username, user_agent)
135
  expiration_time = datetime.now(timezone.utc) + timedelta(minutes=TOKEN_EXPIRATION_MINUTES)
136
 
137
+ session_data = {
138
+ "user_id": user["id"],
139
+ "token": token,
140
+ "expires": expiration_time.isoformat(),
141
+ "device": user_agent
142
+ }
143
+
144
+ supabase.table("sessions").insert(session_data).execute()
145
 
 
146
  return LoginResponse(
147
+ user_id=user["id"],
148
  username=user["username"],
149
  email=user["email"],
150
  access_level=user["access_level"],
151
+ date_joined=datetime.fromisoformat(user["date_joined"]),
152
  access_token=token
153
  )
154
 
155
  @auth_router.post("/logout")
156
+ async def logout(user_id: str, token: str):
157
+ supabase.table("sessions").delete().eq("user_id", user_id).eq("token", token).execute()
 
 
 
 
 
 
 
 
158
  return {"message": "Session forcefully expired"}
159
 
160
  @auth_router.get("/validate", response_model=TokenResponse)
161
+ async def validate_token(user_id: str, token: str, user_agent: str = Header(...)):
162
+ session_query = (
163
+ supabase.table("sessions")
164
+ .select("*")
165
+ .eq("user_id", user_id)
166
+ .eq("token", token)
167
+ .eq("device", user_agent)
168
+ .execute()
169
+ )
170
+
171
+ if not session_query.data:
172
  raise HTTPException(
173
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
174
  )
175
 
176
+ session = session_query.data[0]
177
+ if is_token_expired(datetime.fromisoformat(session["expires"])):
178
+ supabase.table("sessions").delete().eq("id", session["id"]).execute()
179
+ raise HTTPException(
180
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired"
181
+ )
182
+
183
+ return TokenResponse(access_token=token)
 
 
 
 
 
 
 
 
 
 
 
184
 
185
  @auth_router.get("/search-users", response_model=List[str])
186
+ async def search_users(query: str):
187
+ users = supabase.table("users").select("username").ilike("username", f"%{query}%").execute()
188
+ return [user["username"] for user in users.data]
189
+
190
+ @auth_router.get("/get-user-id", response_model=str)
191
+ async def get_user_id(username: str):
192
+ user_query = supabase.table("users").select("id").eq("username", username).execute()
193
+
194
+ if not user_query.data:
195
+ raise HTTPException(
196
+ status_code=status.HTTP_404_NOT_FOUND,
197
+ detail="Username not found"
198
+ )
199
+
200
+ return user_query.data[0]["id"]
201
 
202
  # Admin Routes
203
  @admin_router.get("/users", response_model=List[UserResponse])
204
+ async def get_all_users(user_id: str, token: str, user_agent: str = Header(...)):
205
+ await validate_token(user_id, token, user_agent)
206
 
207
+ admin_query = supabase.table("users").select("access_level").eq("id", user_id).execute()
208
+ if not admin_query.data or admin_query.data[0]["access_level"] != "hush":
209
  raise HTTPException(
210
  status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
211
  )
212
 
213
+ users = supabase.table("users").select("*").execute()
214
  return [
215
+ UserResponse(
216
+ username=user["username"],
217
+ email=user["email"],
218
+ access_level=user["access_level"],
219
+ date_joined=datetime.fromisoformat(user["date_joined"])
220
+ )
221
+ for user in users.data
222
  ]
223
 
224
  @admin_router.get("/user/{user_id}", response_model=UserResponse)
225
+ async def get_user(admin_id: str, token: str, user_id: str, user_agent: str = Header(...)):
226
+ await validate_token(admin_id, token, user_agent)
227
 
228
+ admin_query = supabase.table("users").select("access_level").eq("id", admin_id).execute()
229
+ if not admin_query.data or admin_query.data[0]["access_level"] != "hush":
230
  raise HTTPException(
231
  status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
232
  )
233
 
234
+ user_query = supabase.table("users").select("*").eq("id", user_id).execute()
235
+ if not user_query.data:
236
  raise HTTPException(
237
  status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
238
  )
239
+
240
+ user = user_query.data[0]
241
+ return UserResponse(
242
+ username=user["username"],
243
+ email=user["email"],
244
+ access_level=user["access_level"],
245
+ date_joined=datetime.fromisoformat(user["date_joined"])
246
+ )
247
 
248
  @admin_router.put("/user/{user_id}", response_model=UserResponse)
249
+ async def update_user(admin_id: str, token: str, user_id: str, request: UpdateUserRequest, user_agent: str = Header(...)):
250
+ await validate_token(admin_id, token, user_agent)
251
 
252
+ admin_query = supabase.table("users").select("access_level").eq("id", admin_id).execute()
253
+ if not admin_query.data or admin_query.data[0]["access_level"] != "hush":
254
  raise HTTPException(
255
  status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
256
  )
257
 
258
+ update_data = {}
259
+ if request.password:
260
+ update_data["password"] = hash_password(request.password)
261
+ if request.email:
262
+ update_data["email"] = request.email
263
+ if request.username:
264
+ update_data["username"] = request.username
265
+
266
+ updated_user = supabase.table("users").update(update_data).eq("id", user_id).execute()
267
+ if not updated_user.data:
268
  raise HTTPException(
269
  status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
270
  )
271
 
272
+ user = updated_user.data[0]
273
+ return UserResponse(
274
+ username=user["username"],
275
+ email=user["email"],
276
+ access_level=user["access_level"],
277
+ date_joined=datetime.fromisoformat(user["date_joined"])
278
+ )
 
 
 
 
 
 
 
 
 
279
 
280
  @admin_router.put("/user/{user_id}/access-level")
281
+ async def update_access_level(admin_id: str, token: str, user_id: str, request: UpdateAccessLevelRequest, user_agent: str = Header(...)):
282
+ await validate_token(admin_id, token, user_agent)
283
 
284
+ admin_query = supabase.table("users").select("access_level").eq("id", admin_id).execute()
285
+ if not admin_query.data or admin_query.data[0]["access_level"] != "hush":
286
  raise HTTPException(
287
  status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions"
288
  )
289
 
290
+ user_query = supabase.table("users").select("*").eq("id", user_id).execute()
291
+ if not user_query.data:
292
  raise HTTPException(
293
  status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
294
  )
295
 
296
+ user = user_query.data[0]
297
  new_access_level = request.access_level
298
 
 
299
  if ACCESS_LEVELS.index(new_access_level) <= ACCESS_LEVELS.index(user["access_level"]):
300
  raise HTTPException(
301
  status_code=status.HTTP_400_BAD_REQUEST,
302
  detail="Cannot downgrade a user or change to the same level",
303
  )
304
 
305
+ updated_user = supabase.table("users").update({"access_level": new_access_level}).eq("id", user_id).execute()
306
+ user = updated_user.data[0]
307
+ return UserResponse(
308
+ username=user["username"],
309
+ email=user["email"],
310
+ access_level=user["access_level"],
311
+ date_joined=datetime.fromisoformat(user["date_joined"])
312
+ )
 
 
313
 
 
314
  @auth_router.put("/user/update", response_model=UserResponse)
315
+ async def update_own_data(user_id: str, request: UpdateUserRequest, token: str = Header(...), user_agent: str = Header(...)):
316
+ await validate_token(user_id, token, user_agent)
317
 
318
+ update_data = {}
 
 
 
 
 
 
319
  if request.password:
320
+ update_data["password"] = hash_password(request.password)
321
  if request.email:
322
+ update_data["email"] = request.email
323
  if request.username:
324
+ update_data["username"] = request.username
325
 
326
+ updated_user = supabase.table("users").update(update_data).eq("id", user_id).execute()
327
+ if not updated_user.data:
328
+ raise HTTPException(
329
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
330
+ )
331
 
332
+ user = updated_user.data[0]
333
+ return UserResponse(
334
+ username=user["username"],
335
+ email=user["email"],
336
+ access_level=user["access_level"],
337
+ date_joined=datetime.fromisoformat(user["date_joined"])
338
+ )
339
 
340
  # Include routes
341
  app.include_router(auth_router)
342
  app.include_router(admin_router)
343
+
344
+ # Initialize system user on startup
345
+ @app.on_event("startup")
346
+ async def startup_event():
347
+ await init_system_user()
requirements.txt CHANGED
@@ -2,4 +2,5 @@ fastapi
2
  pydantic[email]
3
  user-agents
4
  python-dotenv
5
- uvicorn[standard]
 
 
2
  pydantic[email]
3
  user-agents
4
  python-dotenv
5
+ uvicorn[standard]
6
+ supabase