Spaces:
Running
Running
debug
Browse files- Dockerfile +7 -11
- app/api.py +24 -61
- app/dependencies.py +29 -13
- app/main.py +50 -220
- requirements.txt +4 -5
- static/css/style.css +94 -0
- static/js/script.js +241 -0
- streamlit_app.py +0 -269
- templates/index.html +56 -0
Dockerfile
CHANGED
@@ -1,24 +1,20 @@
|
|
1 |
# Use an official Python runtime as a parent image
|
2 |
-
FROM python:3.12-slim
|
3 |
|
4 |
-
# Set the working directory in the container
|
5 |
WORKDIR /code
|
6 |
|
7 |
-
# Copy the requirements file into the container
|
8 |
COPY ./requirements.txt /code/requirements.txt
|
9 |
|
10 |
-
# Install any needed packages specified in requirements.txt
|
11 |
RUN pip install --no-cache-dir --upgrade pip && \
|
12 |
pip install --no-cache-dir -r requirements.txt
|
13 |
|
14 |
-
# Copy
|
15 |
COPY ./app /code/app
|
16 |
-
|
|
|
|
|
17 |
|
18 |
-
# Make port 7860 available (Streamlit default is 8501, but HF uses specified port)
|
19 |
EXPOSE 7860
|
20 |
|
21 |
-
# Command to run the
|
22 |
-
|
23 |
-
# Use --server.address to bind correctly inside container
|
24 |
-
CMD ["streamlit", "run", "streamlit_app.py", "--server.port=7860", "--server.address=0.0.0.0"]
|
|
|
1 |
# Use an official Python runtime as a parent image
|
2 |
+
FROM python:3.12-slim # Or 3.10/3.11 if preferred
|
3 |
|
|
|
4 |
WORKDIR /code
|
5 |
|
|
|
6 |
COPY ./requirements.txt /code/requirements.txt
|
7 |
|
|
|
8 |
RUN pip install --no-cache-dir --upgrade pip && \
|
9 |
pip install --no-cache-dir -r requirements.txt
|
10 |
|
11 |
+
# Copy application code
|
12 |
COPY ./app /code/app
|
13 |
+
# Copy static files and templates
|
14 |
+
COPY ./static /code/static
|
15 |
+
COPY ./templates /code/templates
|
16 |
|
|
|
17 |
EXPOSE 7860
|
18 |
|
19 |
+
# Command to run the FastAPI application
|
20 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
app/api.py
CHANGED
@@ -1,97 +1,60 @@
|
|
|
|
1 |
from fastapi import APIRouter, HTTPException, status, Depends, WebSocket, WebSocketDisconnect
|
2 |
-
|
3 |
import logging
|
4 |
|
5 |
from . import schemas, crud, auth, models
|
6 |
from .websocket import manager
|
|
|
7 |
from .dependencies import get_required_current_user
|
8 |
|
9 |
router = APIRouter()
|
10 |
logger = logging.getLogger(__name__)
|
11 |
|
12 |
-
|
|
|
13 |
async def register_user(user_in: schemas.UserCreate):
|
|
|
14 |
existing_user = await crud.get_user_by_email(user_in.email)
|
15 |
if existing_user:
|
16 |
-
raise HTTPException(
|
17 |
-
status_code=status.HTTP_400_BAD_REQUEST,
|
18 |
-
detail="Email already registered",
|
19 |
-
)
|
20 |
hashed_password = auth.get_password_hash(user_in.password)
|
21 |
user_id = await crud.create_user(user_in=user_in, hashed_password=hashed_password)
|
22 |
-
|
23 |
-
# Send notification to other connected users
|
24 |
-
notification_msg = schemas.Notification(
|
25 |
-
email=user_in.email,
|
26 |
-
message=f"New user registered: {user_in.email}"
|
27 |
-
).model_dump_json() # Use model_dump_json for Pydantic v2
|
28 |
-
|
29 |
-
# We broadcast but conceptually exclude the sender.
|
30 |
-
# Since the new user isn't connected via WebSocket *yet* during registration,
|
31 |
-
# we don't have a sender_id from the WebSocket context here.
|
32 |
-
# We can pass the new user_id to prevent potential self-notification if
|
33 |
-
# the WebSocket connection happens very quickly and maps the ID.
|
34 |
await manager.broadcast(notification_msg, sender_id=user_id)
|
35 |
-
|
36 |
-
# Return the newly created user's public info
|
37 |
-
# Fetch the user details to return them accurately
|
38 |
created_user = await crud.get_user_by_id(user_id)
|
39 |
-
if not created_user:
|
40 |
-
# This case should ideally not happen if create_user is successful
|
41 |
-
raise HTTPException(status_code=500, detail="Failed to retrieve created user")
|
42 |
-
|
43 |
-
# Convert UserInDB to User model for response
|
44 |
return models.User(id=created_user.id, email=created_user.email)
|
45 |
|
46 |
-
|
47 |
-
@router.post("/login", response_model=schemas.Token)
|
48 |
async def login_for_access_token(form_data: schemas.UserLogin):
|
|
|
49 |
user = await crud.get_user_by_email(form_data.email)
|
50 |
if not user or not auth.verify_password(form_data.password, user.hashed_password):
|
51 |
-
raise HTTPException(
|
52 |
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
53 |
-
detail="Incorrect email or password",
|
54 |
-
headers={"WWW-Authenticate": "Bearer"},
|
55 |
-
)
|
56 |
access_token = auth.create_session_token(user_id=user.id)
|
57 |
return {"access_token": access_token, "token_type": "bearer"}
|
58 |
|
|
|
59 |
@router.get("/users/me", response_model=models.User)
|
60 |
async def read_users_me(current_user: models.User = Depends(get_required_current_user)):
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
#
|
66 |
return current_user
|
67 |
|
68 |
-
|
69 |
-
# WebSocket endpoint (can be associated with the main API router or separate)
|
70 |
@router.websocket("/ws/{user_id_token}")
|
71 |
async def websocket_endpoint(websocket: WebSocket, user_id_token: str):
|
72 |
-
|
73 |
-
WebSocket endpoint. Connects user and listens for messages.
|
74 |
-
The user_id_token is the signed session token from login.
|
75 |
-
"""
|
76 |
user_id = await auth.get_user_id_from_token(user_id_token)
|
77 |
if user_id is None:
|
78 |
logger.warning(f"WebSocket connection rejected: Invalid token {user_id_token}")
|
79 |
-
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
80 |
-
return
|
81 |
-
|
82 |
await manager.connect(websocket, user_id)
|
83 |
try:
|
84 |
-
while True:
|
85 |
-
|
86 |
-
|
87 |
-
# For now, we just broadcast on registration, not handle client messages
|
88 |
-
logger.debug(f"Received message from {user_id}: {data} (currently ignored)")
|
89 |
-
# Example: await websocket.send_text(f"Message text was: {data}")
|
90 |
-
except WebSocketDisconnect:
|
91 |
-
manager.disconnect(websocket)
|
92 |
-
logger.info(f"WebSocket disconnected for user {user_id}")
|
93 |
-
except Exception as e:
|
94 |
-
manager.disconnect(websocket)
|
95 |
-
logger.error(f"WebSocket error for user {user_id}: {e}")
|
96 |
-
# Optionally close with an error code
|
97 |
-
# await websocket.close(code=status.WS_1011_INTERNAL_ERROR)
|
|
|
1 |
+
# app/api.py
|
2 |
from fastapi import APIRouter, HTTPException, status, Depends, WebSocket, WebSocketDisconnect
|
3 |
+
# Remove JSONResponse if not explicitly needed
|
4 |
import logging
|
5 |
|
6 |
from . import schemas, crud, auth, models
|
7 |
from .websocket import manager
|
8 |
+
# --- Use the new dependency ---
|
9 |
from .dependencies import get_required_current_user
|
10 |
|
11 |
router = APIRouter()
|
12 |
logger = logging.getLogger(__name__)
|
13 |
|
14 |
+
# --- (register and login endpoints remain the same) ---
|
15 |
+
@router.post("/register", ...) # Keep as is
|
16 |
async def register_user(user_in: schemas.UserCreate):
|
17 |
+
# ... same logic ...
|
18 |
existing_user = await crud.get_user_by_email(user_in.email)
|
19 |
if existing_user:
|
20 |
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
|
|
|
|
|
|
|
21 |
hashed_password = auth.get_password_hash(user_in.password)
|
22 |
user_id = await crud.create_user(user_in=user_in, hashed_password=hashed_password)
|
23 |
+
notification_msg = schemas.Notification(email=user_in.email, message=f"New user registered: {user_in.email}").model_dump_json()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
await manager.broadcast(notification_msg, sender_id=user_id)
|
|
|
|
|
|
|
25 |
created_user = await crud.get_user_by_id(user_id)
|
26 |
+
if not created_user: raise HTTPException(status_code=500, detail="Failed to retrieve created user")
|
|
|
|
|
|
|
|
|
27 |
return models.User(id=created_user.id, email=created_user.email)
|
28 |
|
29 |
+
@router.post("/login", ...) # Keep as is
|
|
|
30 |
async def login_for_access_token(form_data: schemas.UserLogin):
|
31 |
+
# ... same logic ...
|
32 |
user = await crud.get_user_by_email(form_data.email)
|
33 |
if not user or not auth.verify_password(form_data.password, user.hashed_password):
|
34 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", headers={"WWW-Authenticate": "Bearer"})
|
|
|
|
|
|
|
|
|
35 |
access_token = auth.create_session_token(user_id=user.id)
|
36 |
return {"access_token": access_token, "token_type": "bearer"}
|
37 |
|
38 |
+
# --- UPDATE this endpoint ---
|
39 |
@router.get("/users/me", response_model=models.User)
|
40 |
async def read_users_me(current_user: models.User = Depends(get_required_current_user)):
|
41 |
+
"""
|
42 |
+
Returns the current authenticated user's details based on the
|
43 |
+
Authorization: Bearer <token> header.
|
44 |
+
"""
|
45 |
+
# The dependency now handles getting the user from the header token
|
46 |
return current_user
|
47 |
|
48 |
+
# --- (websocket endpoint remains the same) ---
|
|
|
49 |
@router.websocket("/ws/{user_id_token}")
|
50 |
async def websocket_endpoint(websocket: WebSocket, user_id_token: str):
|
51 |
+
# ... same logic ...
|
|
|
|
|
|
|
52 |
user_id = await auth.get_user_id_from_token(user_id_token)
|
53 |
if user_id is None:
|
54 |
logger.warning(f"WebSocket connection rejected: Invalid token {user_id_token}")
|
55 |
+
await websocket.close(code=status.WS_1008_POLICY_VIOLATION); return
|
|
|
|
|
56 |
await manager.connect(websocket, user_id)
|
57 |
try:
|
58 |
+
while True: data = await websocket.receive_text(); logger.debug(f"Received WS msg from {user_id}: {data}")
|
59 |
+
except WebSocketDisconnect: manager.disconnect(websocket); logger.info(f"WebSocket disconnected for user {user_id}")
|
60 |
+
except Exception as e: manager.disconnect(websocket); logger.error(f"WebSocket error for user {user_id}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/dependencies.py
CHANGED
@@ -1,22 +1,38 @@
|
|
1 |
-
from fastapi import HTTPException, status
|
|
|
2 |
from typing import Optional
|
3 |
-
from . import auth
|
4 |
-
from .models import User
|
5 |
|
6 |
-
#
|
7 |
-
#
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
user = await auth.get_current_user_from_token(token)
|
12 |
return user
|
13 |
-
|
|
|
14 |
|
15 |
-
async def get_required_current_user(token:
|
16 |
-
|
|
|
|
|
|
|
|
|
17 |
if user is None:
|
|
|
18 |
raise HTTPException(
|
19 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
20 |
-
detail="
|
|
|
21 |
)
|
22 |
-
return user
|
|
|
|
|
|
1 |
+
from fastapi import Depends, HTTPException, status
|
2 |
+
from fastapi.security import OAuth2PasswordBearer # Use FastAPI's built-in helper
|
3 |
from typing import Optional
|
4 |
+
from . import auth, models
|
|
|
5 |
|
6 |
+
# Setup OAuth2 scheme pointing to the login *API* endpoint
|
7 |
+
# tokenUrl relative to the path where the app is mounted
|
8 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/login")
|
9 |
+
|
10 |
+
async def get_optional_current_user(token: str = Depends(oauth2_scheme)) -> Optional[models.User]:
|
11 |
+
"""
|
12 |
+
Dependency to get the current user from the token in the Authorization header.
|
13 |
+
Returns None if the token is invalid or not provided.
|
14 |
+
Handles potential exceptions during token decoding/validation gracefully for optional user.
|
15 |
+
"""
|
16 |
+
try:
|
17 |
+
# OAuth2PasswordBearer already extracts the token from the header
|
18 |
user = await auth.get_current_user_from_token(token)
|
19 |
return user
|
20 |
+
except Exception: # Catch exceptions if the token is invalid but we don't want to fail hard
|
21 |
+
return None
|
22 |
|
23 |
+
async def get_required_current_user(token: str = Depends(oauth2_scheme)) -> models.User:
|
24 |
+
"""
|
25 |
+
Dependency to get the current user, raising HTTP 401 if not authenticated.
|
26 |
+
"""
|
27 |
+
# OAuth2PasswordBearer will raise a 401 if the header is missing/malformed
|
28 |
+
user = await auth.get_current_user_from_token(token)
|
29 |
if user is None:
|
30 |
+
# This case covers valid token format but expired/invalid signature/user not found
|
31 |
raise HTTPException(
|
32 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
33 |
+
detail="Could not validate credentials", # Keep detail generic
|
34 |
+
headers={"WWW-Authenticate": "Bearer"},
|
35 |
)
|
36 |
+
return user
|
37 |
+
|
38 |
+
# Modify the /users/me endpoint in api.py to use the new dependency
|
app/main.py
CHANGED
@@ -1,31 +1,41 @@
|
|
1 |
# app/main.py
|
2 |
-
|
3 |
-
import
|
4 |
-
|
|
|
|
|
5 |
import asyncio
|
6 |
import json
|
7 |
import os
|
8 |
import logging
|
9 |
from contextlib import asynccontextmanager
|
10 |
|
11 |
-
from fastapi import FastAPI, Depends #
|
|
|
|
|
|
|
|
|
|
|
12 |
from .database import connect_db, disconnect_db, database, metadata, users
|
13 |
from .api import router as api_router
|
14 |
from . import schemas, auth, dependencies
|
15 |
-
from .websocket import manager #
|
16 |
|
|
|
17 |
from sqlalchemy.schema import CreateTable
|
18 |
from sqlalchemy.dialects import sqlite
|
19 |
|
|
|
20 |
logging.basicConfig(level=logging.INFO)
|
21 |
logger = logging.getLogger(__name__)
|
22 |
|
23 |
-
API_BASE_URL
|
|
|
24 |
|
25 |
-
# --- Lifespan (remains the same) ---
|
26 |
@asynccontextmanager
|
27 |
async def lifespan(app: FastAPI):
|
28 |
-
# ... (same DB setup code) ...
|
29 |
logger.info("Application startup: Connecting DB...")
|
30 |
await connect_db()
|
31 |
logger.info("Application startup: DB Connected. Checking/Creating tables...")
|
@@ -34,16 +44,16 @@ async def lifespan(app: FastAPI):
|
|
34 |
check_query = "SELECT name FROM sqlite_master WHERE type='table' AND name=:table_name;"
|
35 |
table_exists = await database.fetch_one(query=check_query, values={"table_name": users.name})
|
36 |
if not table_exists:
|
37 |
-
logger.info(f"Table '{users.name}' not found,
|
38 |
dialect = sqlite.dialect()
|
39 |
create_table_stmt = str(CreateTable(users).compile(dialect=dialect))
|
40 |
await database.execute(query=create_table_stmt)
|
41 |
-
logger.info(f"Table '{users.name}' created
|
42 |
table_exists_after = await database.fetch_one(query=check_query, values={"table_name": users.name})
|
43 |
-
if table_exists_after: logger.info(f"Table '{users.name}' verified
|
44 |
-
else: logger.error(f"Table '{users.name}' verification FAILED
|
45 |
else:
|
46 |
-
logger.info(f"Table '{users.name}' already exists
|
47 |
except Exception as db_setup_err:
|
48 |
logger.exception(f"CRITICAL error during async DB table setup: {db_setup_err}")
|
49 |
else:
|
@@ -55,222 +65,42 @@ async def lifespan(app: FastAPI):
|
|
55 |
logger.info("Application shutdown: DB Disconnected.")
|
56 |
|
57 |
|
58 |
-
#
|
59 |
app = FastAPI(lifespan=lifespan)
|
60 |
-
app.include_router(api_router, prefix="/api")
|
61 |
-
|
62 |
-
# --- Helper functions (make_api_request remains the same) ---
|
63 |
-
async def make_api_request(method: str, endpoint: str, **kwargs):
|
64 |
-
async with httpx.AsyncClient() as client:
|
65 |
-
url = f"{API_BASE_URL}{endpoint}"
|
66 |
-
try: response = await client.request(method, url, **kwargs); response.raise_for_status(); return response.json()
|
67 |
-
except httpx.RequestError as e: logger.error(f"HTTP Request failed: {e.request.method} {e.request.url} - {e}"); return {"error": f"Network error: {e}"}
|
68 |
-
except httpx.HTTPStatusError as e:
|
69 |
-
logger.error(f"HTTP Status error: {e.response.status_code} - {e.response.text}")
|
70 |
-
try: detail = e.response.json().get("detail", e.response.text)
|
71 |
-
except json.JSONDecodeError: detail = e.response.text
|
72 |
-
return {"error": f"API Error: {detail}"}
|
73 |
-
except Exception as e: logger.error(f"Unexpected error during API call: {e}"); return {"error": f"Unexpected error: {str(e)}"}
|
74 |
|
75 |
-
#
|
76 |
-
|
77 |
-
async def listen_to_websockets(token: str, notification_list_state: gr.State, notification_trigger_state: gr.State):
|
78 |
-
"""Connects to WS and updates state list and trigger when a message arrives."""
|
79 |
-
ws_listener_id = f"WSListener-{os.getpid()}-{asyncio.current_task().get_name()}"
|
80 |
-
logger.info(f"[{ws_listener_id}] Starting WebSocket listener task.")
|
81 |
|
82 |
-
|
83 |
-
|
84 |
-
|
|
|
|
|
85 |
|
86 |
-
|
87 |
-
|
88 |
-
logger.info(f"[{ws_listener_id}] Attempting to connect: {ws_url}")
|
89 |
|
|
|
|
|
|
|
|
|
90 |
try:
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
if message_data.get("type") == "new_user":
|
100 |
-
notification = schemas.Notification(**message_data)
|
101 |
-
logger.info(f"[{ws_listener_id}] Processing 'new_user': {notification.message}")
|
102 |
-
# --- Modify state values directly ---
|
103 |
-
current_list = notification_list_state.value.copy() # Operate on copy
|
104 |
-
current_list.insert(0, notification.message)
|
105 |
-
if len(current_list) > 10: current_list.pop()
|
106 |
-
notification_list_state.value = current_list # Assign back modified copy
|
107 |
-
notification_trigger_state.value += 1 # Increment trigger
|
108 |
-
# --- Log the update ---
|
109 |
-
logger.info(f"[{ws_listener_id}] States updated: list len={len(notification_list_state.value)}, trigger={notification_trigger_state.value}")
|
110 |
-
else:
|
111 |
-
logger.warning(f"[{ws_listener_id}] Unknown message type: {message_data.get('type')}")
|
112 |
-
# ... (error handling for parsing) ...
|
113 |
-
except json.JSONDecodeError: logger.error(f"[{ws_listener_id}] JSON Decode Error: {message_str}")
|
114 |
-
except Exception as parse_err: logger.error(f"[{ws_listener_id}] Message Processing Error: {parse_err}")
|
115 |
-
# ... (error handling for websocket recv/connection) ...
|
116 |
-
except asyncio.TimeoutError: logger.debug(f"[{ws_listener_id}] WebSocket recv timed out."); continue
|
117 |
-
except websockets.ConnectionClosedOK: logger.info(f"[{ws_listener_id}] WebSocket closed normally."); break
|
118 |
-
except websockets.ConnectionClosedError as e: logger.error(f"[{ws_listener_id}] WebSocket closed with error: {e}"); break
|
119 |
-
except Exception as e: logger.error(f"[{ws_listener_id}] Listener loop error: {e}"); await asyncio.sleep(1); break
|
120 |
-
# ... (error handling for websocket connect) ...
|
121 |
-
except asyncio.TimeoutError: logger.error(f"[{ws_listener_id}] WebSocket initial connection timed out.")
|
122 |
-
except websockets.exceptions.InvalidURI: logger.error(f"[{ws_listener_id}] Invalid WebSocket URI.")
|
123 |
-
except websockets.exceptions.WebSocketException as e: logger.error(f"[{ws_listener_id}] WebSocket connection failed: {e}")
|
124 |
-
except Exception as e: logger.error(f"[{ws_listener_id}] Unexpected error in listener task: {e}")
|
125 |
-
|
126 |
-
logger.info(f"[{ws_listener_id}] Listener task finished.")
|
127 |
-
# No need to return state values when task ends
|
128 |
-
|
129 |
-
# --- Gradio Interface ---
|
130 |
-
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
131 |
-
# State variables
|
132 |
-
auth_token = gr.State(None)
|
133 |
-
user_info = gr.State(None)
|
134 |
-
notification_list = gr.State([])
|
135 |
-
websocket_task = gr.State(None)
|
136 |
-
# Add trigger states
|
137 |
-
notification_trigger = gr.State(0)
|
138 |
-
last_polled_trigger = gr.State(0) # State to track last seen trigger
|
139 |
-
|
140 |
-
# --- UI Components ---
|
141 |
-
with gr.Tabs() as tabs:
|
142 |
-
# --- Registration/Login Tabs (remain the same) ---
|
143 |
-
with gr.TabItem("Register", id="register_tab"):
|
144 |
-
gr.Markdown("## Create a new account")
|
145 |
-
reg_email = gr.Textbox(label="Email", type="email")
|
146 |
-
reg_password = gr.Textbox(label="Password (min 8 chars)", type="password")
|
147 |
-
reg_confirm_password = gr.Textbox(label="Confirm Password", type="password")
|
148 |
-
reg_button = gr.Button("Register")
|
149 |
-
reg_status = gr.Textbox(label="Status", interactive=False)
|
150 |
-
with gr.TabItem("Login", id="login_tab"):
|
151 |
-
gr.Markdown("## Login to your account")
|
152 |
-
login_email = gr.Textbox(label="Email", type="email")
|
153 |
-
login_password = gr.Textbox(label="Password", type="password")
|
154 |
-
login_button = gr.Button("Login")
|
155 |
-
login_status = gr.Textbox(label="Status", interactive=False)
|
156 |
-
|
157 |
-
# --- Welcome Tab ---
|
158 |
-
with gr.TabItem("Welcome", id="welcome_tab", visible=False) as welcome_tab:
|
159 |
-
gr.Markdown("## Welcome!", elem_id="welcome_header")
|
160 |
-
welcome_message = gr.Markdown("", elem_id="welcome_message")
|
161 |
-
logout_button = gr.Button("Logout")
|
162 |
-
gr.Markdown("---")
|
163 |
-
gr.Markdown("## Real-time Notifications")
|
164 |
-
notification_display = gr.Textbox( # Visible display
|
165 |
-
label="New User Alerts", lines=5, max_lines=10, interactive=False
|
166 |
-
)
|
167 |
-
# <<< Hidden component for polling >>>
|
168 |
-
dummy_poller = gr.Number(label="poller", value=0, visible=False, every=1)
|
169 |
-
|
170 |
-
|
171 |
-
# --- Event Handlers ---
|
172 |
-
|
173 |
-
# Registration Logic (remains the same)
|
174 |
-
async def handle_register(email, password, confirm_password):
|
175 |
-
if not email or not password or not confirm_password: return gr.update(value="Please fill fields.")
|
176 |
-
if password != confirm_password: return gr.update(value="Passwords mismatch.")
|
177 |
-
if len(password) < 8: return gr.update(value="Password >= 8 chars.")
|
178 |
-
payload = {"email": email, "password": password}
|
179 |
-
result = await make_api_request("post", "/register", json=payload)
|
180 |
-
if "error" in result: return gr.update(value=f"Register failed: {result['error']}")
|
181 |
-
else: return gr.update(value=f"Register success: {result.get('email')}! Log in.")
|
182 |
-
reg_button.click(handle_register, inputs=[reg_email, reg_password, reg_confirm_password], outputs=[reg_status])
|
183 |
-
|
184 |
-
# Login Logic
|
185 |
-
# <<< MODIFIED: Pass/Reset both trigger states >>>
|
186 |
-
async def handle_login(email, password, current_task, current_trigger_val, current_last_poll_val):
|
187 |
-
# Define failure outputs matching the outputs list
|
188 |
-
fail_outputs = (gr.update(value="..."), None, None, None, gr.update(visible=False), None, current_task, current_trigger_val, current_last_poll_val)
|
189 |
-
|
190 |
-
if not email or not password: return fail_outputs[:1] + (gr.update(value="Enter email/password"),) + fail_outputs[2:]
|
191 |
-
|
192 |
-
payload = {"email": email, "password": password}
|
193 |
-
result = await make_api_request("post", "/login", json=payload)
|
194 |
-
|
195 |
-
if "error" in result: return (gr.update(value=f"Login failed: {result['error']}"),) + fail_outputs[1:]
|
196 |
-
else:
|
197 |
-
token = result.get("access_token")
|
198 |
-
user_data = await dependencies.get_optional_current_user(token)
|
199 |
-
if not user_data: return (gr.update(value="Login ok but user fetch failed."),) + fail_outputs[1:]
|
200 |
-
|
201 |
-
if current_task and not current_task.done():
|
202 |
-
current_task.cancel()
|
203 |
-
try: await current_task
|
204 |
-
except asyncio.CancelledError: logger.info("Previous WebSocket task cancelled.")
|
205 |
-
|
206 |
-
# <<< Pass state objects to listener >>>
|
207 |
-
new_task = asyncio.create_task(listen_to_websockets(token, notification_list, notification_trigger))
|
208 |
-
|
209 |
-
welcome_msg = f"Welcome, {user_data.email}!"
|
210 |
-
# Reset triggers on successful login
|
211 |
-
return (
|
212 |
-
gr.update(value="Login successful!"), token, user_data.model_dump(), # status, token, user_info
|
213 |
-
gr.update(selected="welcome_tab"), gr.update(visible=True), gr.update(value=welcome_msg), # UI changes
|
214 |
-
new_task, 0, 0 # websocket_task, notification_trigger, last_polled_trigger
|
215 |
-
)
|
216 |
-
|
217 |
-
# <<< MODIFIED: Add last_polled_trigger to inputs/outputs >>>
|
218 |
-
login_button.click(
|
219 |
-
handle_login,
|
220 |
-
inputs=[login_email, login_password, websocket_task, notification_trigger, last_polled_trigger],
|
221 |
-
outputs=[login_status, auth_token, user_info, tabs, welcome_tab, welcome_message, websocket_task, notification_trigger, last_polled_trigger]
|
222 |
-
)
|
223 |
-
|
224 |
-
|
225 |
-
# <<< Polling function >>>
|
226 |
-
def poll_and_update(current_trigger_value, last_known_trigger, current_notif_list):
|
227 |
-
""" Checks trigger state change and updates UI if needed. """
|
228 |
-
if current_trigger_value != last_known_trigger:
|
229 |
-
logger.info(f"Polling detected trigger change ({last_known_trigger} -> {current_trigger_value}). Updating UI.")
|
230 |
-
new_value = "\n".join(current_notif_list)
|
231 |
-
# Return new display value AND update last_known_trigger state value
|
232 |
-
return gr.update(value=new_value), current_trigger_value
|
233 |
-
else:
|
234 |
-
# No change, return NoUpdate for display AND existing last_known_trigger value
|
235 |
-
return gr.NoUpdate(), last_known_trigger
|
236 |
-
|
237 |
-
# <<< Attach polling function to the dummy component's change event >>>
|
238 |
-
dummy_poller.change(
|
239 |
-
fn=poll_and_update,
|
240 |
-
inputs=[notification_trigger, last_polled_trigger, notification_list],
|
241 |
-
outputs=[notification_display, last_polled_trigger], # Update display & last polled state
|
242 |
-
queue=False # Try immediate update
|
243 |
-
)
|
244 |
-
|
245 |
-
|
246 |
-
# Logout Logic
|
247 |
-
# <<< MODIFIED: Reset both trigger states >>>
|
248 |
-
async def handle_logout(current_task):
|
249 |
-
if current_task and not current_task.done():
|
250 |
-
current_task.cancel()
|
251 |
-
try: await current_task
|
252 |
-
except asyncio.CancelledError: logger.info("WebSocket task cancelled on logout.")
|
253 |
-
# Reset all relevant states
|
254 |
-
return ( None, None, [], None, # auth_token, user_info, notification_list, websocket_task
|
255 |
-
gr.update(selected="login_tab"), gr.update(visible=False), # tabs, welcome_tab
|
256 |
-
gr.update(value=""), gr.update(value=""), # welcome_message, login_status
|
257 |
-
0, 0 ) # notification_trigger, last_polled_trigger (reset)
|
258 |
|
259 |
-
# <<< MODIFIED: Add last_polled_trigger to outputs >>>
|
260 |
-
logout_button.click(
|
261 |
-
handle_logout,
|
262 |
-
inputs=[websocket_task],
|
263 |
-
outputs=[
|
264 |
-
auth_token, user_info, notification_list, websocket_task,
|
265 |
-
tabs, welcome_tab, welcome_message, login_status,
|
266 |
-
notification_trigger, last_polled_trigger # Add trigger states here
|
267 |
-
]
|
268 |
-
)
|
269 |
|
270 |
-
#
|
271 |
-
app = gr.mount_gradio_app(app, demo, path="/")
|
272 |
|
273 |
-
#
|
274 |
if __name__ == "__main__":
|
275 |
import uvicorn
|
|
|
|
|
276 |
uvicorn.run("app.main:app", host="0.0.0.0", port=7860, reload=True)
|
|
|
1 |
# app/main.py
|
2 |
+
# Remove Gradio imports if any remain
|
3 |
+
# import gradio as gr <--- REMOVE
|
4 |
+
|
5 |
+
import httpx # Keep if needed, but not used in this version of main.py
|
6 |
+
import websockets # Keep if needed, but not used in this version of main.py
|
7 |
import asyncio
|
8 |
import json
|
9 |
import os
|
10 |
import logging
|
11 |
from contextlib import asynccontextmanager
|
12 |
|
13 |
+
from fastapi import FastAPI, Depends, Request # Add Request
|
14 |
+
from fastapi.responses import HTMLResponse # Add HTMLResponse
|
15 |
+
from fastapi.staticfiles import StaticFiles # Add StaticFiles
|
16 |
+
from fastapi.templating import Jinja2Templates # Add Jinja2Templates (optional, but good practice)
|
17 |
+
|
18 |
+
# --- Import necessary items from database.py ---
|
19 |
from .database import connect_db, disconnect_db, database, metadata, users
|
20 |
from .api import router as api_router
|
21 |
from . import schemas, auth, dependencies
|
22 |
+
from .websocket import manager # Keep
|
23 |
|
24 |
+
# --- Import SQLAlchemy helpers for DDL generation ---
|
25 |
from sqlalchemy.schema import CreateTable
|
26 |
from sqlalchemy.dialects import sqlite
|
27 |
|
28 |
+
# Configure logging
|
29 |
logging.basicConfig(level=logging.INFO)
|
30 |
logger = logging.getLogger(__name__)
|
31 |
|
32 |
+
# --- REMOVE API_BASE_URL if not needed elsewhere ---
|
33 |
+
# API_BASE_URL = "http://127.0.0.1:7860/api"
|
34 |
|
35 |
+
# --- Lifespan Event (remains the same) ---
|
36 |
@asynccontextmanager
|
37 |
async def lifespan(app: FastAPI):
|
38 |
+
# ... (same DB setup code as previous correct version) ...
|
39 |
logger.info("Application startup: Connecting DB...")
|
40 |
await connect_db()
|
41 |
logger.info("Application startup: DB Connected. Checking/Creating tables...")
|
|
|
44 |
check_query = "SELECT name FROM sqlite_master WHERE type='table' AND name=:table_name;"
|
45 |
table_exists = await database.fetch_one(query=check_query, values={"table_name": users.name})
|
46 |
if not table_exists:
|
47 |
+
logger.info(f"Table '{users.name}' not found, creating...")
|
48 |
dialect = sqlite.dialect()
|
49 |
create_table_stmt = str(CreateTable(users).compile(dialect=dialect))
|
50 |
await database.execute(query=create_table_stmt)
|
51 |
+
logger.info(f"Table '{users.name}' created.")
|
52 |
table_exists_after = await database.fetch_one(query=check_query, values={"table_name": users.name})
|
53 |
+
if table_exists_after: logger.info(f"Table '{users.name}' verified.")
|
54 |
+
else: logger.error(f"Table '{users.name}' verification FAILED!")
|
55 |
else:
|
56 |
+
logger.info(f"Table '{users.name}' already exists.")
|
57 |
except Exception as db_setup_err:
|
58 |
logger.exception(f"CRITICAL error during async DB table setup: {db_setup_err}")
|
59 |
else:
|
|
|
65 |
logger.info("Application shutdown: DB Disconnected.")
|
66 |
|
67 |
|
68 |
+
# Create the main FastAPI app instance
|
69 |
app = FastAPI(lifespan=lifespan)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
|
71 |
+
# Mount API routes FIRST
|
72 |
+
app.include_router(api_router, prefix="/api")
|
|
|
|
|
|
|
|
|
73 |
|
74 |
+
# --- Mount Static files ---
|
75 |
+
# Ensure the path exists relative to where you run uvicorn (or use absolute paths)
|
76 |
+
# Since main.py is in app/, static/ is one level up
|
77 |
+
# Adjust 'directory' path if needed based on your execution context
|
78 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
79 |
|
80 |
+
# --- Optional: Use Jinja2Templates for more flexibility ---
|
81 |
+
# templates = Jinja2Templates(directory="templates")
|
|
|
82 |
|
83 |
+
# --- Serve the main HTML page ---
|
84 |
+
@app.get("/", response_class=HTMLResponse)
|
85 |
+
async def read_root(request: Request):
|
86 |
+
# Simple way: Read the file directly
|
87 |
try:
|
88 |
+
with open("templates/index.html", "r") as f:
|
89 |
+
html_content = f.read()
|
90 |
+
return HTMLResponse(content=html_content)
|
91 |
+
except FileNotFoundError:
|
92 |
+
logger.error("templates/index.html not found!")
|
93 |
+
return HTMLResponse(content="<html><body><h1>Error: Frontend not found</h1></body></html>", status_code=500)
|
94 |
+
# Jinja2 way (if using templates):
|
95 |
+
# return templates.TemplateResponse("index.html", {"request": request})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
|
98 |
+
# --- REMOVE Gradio mounting ---
|
99 |
+
# app = gr.mount_gradio_app(app, demo, path="/")
|
100 |
|
101 |
+
# --- Uvicorn run command (no changes needed here) ---
|
102 |
if __name__ == "__main__":
|
103 |
import uvicorn
|
104 |
+
# Note: If running from the project root directory (fastapi_gradio_auth/),
|
105 |
+
# the app path is "app.main:app"
|
106 |
uvicorn.run("app.main:app", host="0.0.0.0", port=7860, reload=True)
|
requirements.txt
CHANGED
@@ -1,9 +1,8 @@
|
|
1 |
fastapi==0.111.0
|
2 |
uvicorn[standard]==0.29.0
|
3 |
-
# gradio==4.29.0 #
|
4 |
-
streamlit==1.33.0 # Added (or latest)
|
5 |
-
passlib[bcrypt]==1.7.4
|
6 |
bcrypt==4.1.3
|
|
|
7 |
python-dotenv==1.0.1
|
8 |
databases[sqlite]==0.9.0
|
9 |
sqlalchemy==2.0.29
|
@@ -11,5 +10,5 @@ pydantic==2.7.1
|
|
11 |
python-multipart==0.0.9
|
12 |
itsdangerous==2.1.2
|
13 |
websockets>=11.0.3,<13.0
|
14 |
-
|
15 |
-
|
|
|
1 |
fastapi==0.111.0
|
2 |
uvicorn[standard]==0.29.0
|
3 |
+
# gradio==4.29.0 # REMOVE
|
|
|
|
|
4 |
bcrypt==4.1.3
|
5 |
+
passlib[bcrypt]==1.7.4
|
6 |
python-dotenv==1.0.1
|
7 |
databases[sqlite]==0.9.0
|
8 |
sqlalchemy==2.0.29
|
|
|
10 |
python-multipart==0.0.9
|
11 |
itsdangerous==2.1.2
|
12 |
websockets>=11.0.3,<13.0
|
13 |
+
aiofiles==23.2.1 # <-- ADD for StaticFiles
|
14 |
+
httpx==0.27.0 # <-- ADD (useful if backend needs to call itself, good practice)
|
static/css/style.css
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
font-family: sans-serif;
|
3 |
+
line-height: 1.6;
|
4 |
+
margin: 20px;
|
5 |
+
background-color: #f4f4f4;
|
6 |
+
}
|
7 |
+
|
8 |
+
h1, h2 {
|
9 |
+
color: #333;
|
10 |
+
}
|
11 |
+
|
12 |
+
.auth-section {
|
13 |
+
background-color: #fff;
|
14 |
+
padding: 20px;
|
15 |
+
margin-bottom: 20px;
|
16 |
+
border: 1px solid #ddd;
|
17 |
+
border-radius: 5px;
|
18 |
+
max-width: 400px;
|
19 |
+
}
|
20 |
+
|
21 |
+
#welcome-section {
|
22 |
+
background-color: #e7f3fe;
|
23 |
+
padding: 20px;
|
24 |
+
border: 1px solid #c8e1f8;
|
25 |
+
border-radius: 5px;
|
26 |
+
}
|
27 |
+
|
28 |
+
|
29 |
+
label {
|
30 |
+
display: inline-block;
|
31 |
+
margin-bottom: 5px;
|
32 |
+
}
|
33 |
+
|
34 |
+
input[type="email"],
|
35 |
+
input[type="password"] {
|
36 |
+
width: calc(100% - 22px); /* Account for padding/border */
|
37 |
+
padding: 10px;
|
38 |
+
margin-bottom: 10px;
|
39 |
+
border: 1px solid #ccc;
|
40 |
+
border-radius: 4px;
|
41 |
+
}
|
42 |
+
|
43 |
+
button {
|
44 |
+
background-color: #5cb85c;
|
45 |
+
color: white;
|
46 |
+
padding: 10px 15px;
|
47 |
+
border: none;
|
48 |
+
border-radius: 4px;
|
49 |
+
cursor: pointer;
|
50 |
+
font-size: 1em;
|
51 |
+
}
|
52 |
+
|
53 |
+
button:hover {
|
54 |
+
background-color: #4cae4c;
|
55 |
+
}
|
56 |
+
|
57 |
+
#logout-button {
|
58 |
+
background-color: #d9534f;
|
59 |
+
}
|
60 |
+
#logout-button:hover {
|
61 |
+
background-color: #c9302c;
|
62 |
+
}
|
63 |
+
|
64 |
+
.status-message {
|
65 |
+
margin-top: 15px;
|
66 |
+
color: red;
|
67 |
+
font-weight: bold;
|
68 |
+
}
|
69 |
+
.status-message.success {
|
70 |
+
color: green;
|
71 |
+
}
|
72 |
+
|
73 |
+
hr {
|
74 |
+
margin: 20px 0;
|
75 |
+
}
|
76 |
+
|
77 |
+
#notifications {
|
78 |
+
margin-top: 15px;
|
79 |
+
padding: 15px;
|
80 |
+
border: 1px dashed #aaa;
|
81 |
+
background-color: #f9f9f9;
|
82 |
+
min-height: 100px;
|
83 |
+
max-height: 300px;
|
84 |
+
overflow-y: auto;
|
85 |
+
}
|
86 |
+
|
87 |
+
#notifications p {
|
88 |
+
margin: 5px 0;
|
89 |
+
padding-bottom: 5px;
|
90 |
+
border-bottom: 1px solid #eee;
|
91 |
+
}
|
92 |
+
#notifications p:last-child {
|
93 |
+
border-bottom: none;
|
94 |
+
}
|
static/js/script.js
ADDED
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const registerSection = document.getElementById('register-section');
|
2 |
+
const loginSection = document.getElementById('login-section');
|
3 |
+
const welcomeSection = document.getElementById('welcome-section');
|
4 |
+
const registerForm = document.getElementById('register-form');
|
5 |
+
const loginForm = document.getElementById('login-form');
|
6 |
+
const registerStatus = document.getElementById('register-status');
|
7 |
+
const loginStatus = document.getElementById('login-status');
|
8 |
+
const welcomeMessage = document.getElementById('welcome-message');
|
9 |
+
const logoutButton = document.getElementById('logout-button');
|
10 |
+
const notificationsDiv = document.getElementById('notifications');
|
11 |
+
|
12 |
+
const API_URL = '/api'; // Use relative path
|
13 |
+
|
14 |
+
let webSocket = null;
|
15 |
+
let authToken = localStorage.getItem('authToken'); // Load token on script start
|
16 |
+
|
17 |
+
// --- UI Control ---
|
18 |
+
function showSection(sectionId) {
|
19 |
+
registerSection.style.display = 'none';
|
20 |
+
loginSection.style.display = 'none';
|
21 |
+
welcomeSection.style.display = 'none';
|
22 |
+
|
23 |
+
const sectionToShow = document.getElementById(sectionId);
|
24 |
+
if (sectionToShow) {
|
25 |
+
sectionToShow.style.display = 'block';
|
26 |
+
} else {
|
27 |
+
loginSection.style.display = 'block'; // Default to login
|
28 |
+
}
|
29 |
+
}
|
30 |
+
|
31 |
+
function setStatus(element, message, isSuccess = false) {
|
32 |
+
element.textContent = message;
|
33 |
+
element.className = isSuccess ? 'status-message success' : 'status-message';
|
34 |
+
element.style.display = message ? 'block' : 'none';
|
35 |
+
}
|
36 |
+
|
37 |
+
// --- API Calls ---
|
38 |
+
async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
|
39 |
+
const headers = { 'Content-Type': 'application/json' };
|
40 |
+
if (token) {
|
41 |
+
headers['Authorization'] = `Bearer ${token}`;
|
42 |
+
}
|
43 |
+
|
44 |
+
const options = {
|
45 |
+
method: method,
|
46 |
+
headers: headers,
|
47 |
+
};
|
48 |
+
|
49 |
+
if (body && method !== 'GET') {
|
50 |
+
options.body = JSON.stringify(body);
|
51 |
+
}
|
52 |
+
|
53 |
+
try {
|
54 |
+
const response = await fetch(`${API_URL}${endpoint}`, options);
|
55 |
+
const data = await response.json(); // Try to parse JSON regardless of status
|
56 |
+
|
57 |
+
if (!response.ok) {
|
58 |
+
// Use detail from JSON if available, otherwise status text
|
59 |
+
const errorMessage = data?.detail || response.statusText || `HTTP error ${response.status}`;
|
60 |
+
console.error("API Error:", errorMessage);
|
61 |
+
throw new Error(errorMessage);
|
62 |
+
}
|
63 |
+
return data;
|
64 |
+
} catch (error) {
|
65 |
+
console.error(`Error during API request to ${endpoint}:`, error);
|
66 |
+
throw error; // Re-throw to be caught by caller
|
67 |
+
}
|
68 |
+
}
|
69 |
+
|
70 |
+
|
71 |
+
// --- WebSocket Handling ---
|
72 |
+
function connectWebSocket(token) {
|
73 |
+
if (!token) {
|
74 |
+
console.error("No token available for WebSocket connection.");
|
75 |
+
return;
|
76 |
+
}
|
77 |
+
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
|
78 |
+
console.log("WebSocket already connected.");
|
79 |
+
return;
|
80 |
+
}
|
81 |
+
|
82 |
+
// Construct WebSocket URL dynamically (replace http/https with ws/wss)
|
83 |
+
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
84 |
+
const wsUrl = `${wsProtocol}//${window.location.host}${API_URL}/ws/${token}`;
|
85 |
+
console.log("Attempting to connect WebSocket:", wsUrl);
|
86 |
+
|
87 |
+
|
88 |
+
webSocket = new WebSocket(wsUrl);
|
89 |
+
|
90 |
+
webSocket.onopen = (event) => {
|
91 |
+
console.log("WebSocket connection opened:", event);
|
92 |
+
// Clear placeholder message on successful connect
|
93 |
+
if (notificationsDiv.querySelector('p em')) {
|
94 |
+
notificationsDiv.innerHTML = '';
|
95 |
+
}
|
96 |
+
};
|
97 |
+
|
98 |
+
webSocket.onmessage = (event) => {
|
99 |
+
console.log("WebSocket message received:", event.data);
|
100 |
+
try {
|
101 |
+
const messageData = JSON.parse(event.data);
|
102 |
+
if (messageData.type === 'new_user' && messageData.message) {
|
103 |
+
const p = document.createElement('p');
|
104 |
+
p.textContent = messageData.message;
|
105 |
+
// Add message to the top
|
106 |
+
notificationsDiv.insertBefore(p, notificationsDiv.firstChild);
|
107 |
+
// Limit number of messages shown (optional)
|
108 |
+
while (notificationsDiv.children.length > 10) {
|
109 |
+
notificationsDiv.removeChild(notificationsDiv.lastChild);
|
110 |
+
}
|
111 |
+
|
112 |
+
}
|
113 |
+
} catch (error) {
|
114 |
+
console.error("Error parsing WebSocket message:", error);
|
115 |
+
}
|
116 |
+
};
|
117 |
+
|
118 |
+
webSocket.onerror = (event) => {
|
119 |
+
console.error("WebSocket error:", event);
|
120 |
+
const p = document.createElement('p');
|
121 |
+
p.textContent = `WebSocket error occurred. Notifications may stop.`;
|
122 |
+
p.style.color = 'red';
|
123 |
+
notificationsDiv.insertBefore(p, notificationsDiv.firstChild);
|
124 |
+
};
|
125 |
+
|
126 |
+
webSocket.onclose = (event) => {
|
127 |
+
console.log("WebSocket connection closed:", event);
|
128 |
+
webSocket = null; // Reset WebSocket variable
|
129 |
+
// Optionally try to reconnect or inform user
|
130 |
+
// Do not clear notifications on close, user might want to see history
|
131 |
+
if (!event.wasClean) {
|
132 |
+
const p = document.createElement('p');
|
133 |
+
p.textContent = `WebSocket disconnected unexpectedly (Code: ${event.code}). Please refresh or log out/in.`;
|
134 |
+
p.style.color = 'orange';
|
135 |
+
notificationsDiv.insertBefore(p, notificationsDiv.firstChild);
|
136 |
+
}
|
137 |
+
};
|
138 |
+
}
|
139 |
+
|
140 |
+
function disconnectWebSocket() {
|
141 |
+
if (webSocket) {
|
142 |
+
console.log("Closing WebSocket connection.");
|
143 |
+
webSocket.close();
|
144 |
+
webSocket = null;
|
145 |
+
}
|
146 |
+
}
|
147 |
+
|
148 |
+
// --- Authentication Logic ---
|
149 |
+
async function handleLogin(email, password) {
|
150 |
+
setStatus(loginStatus, "Logging in...");
|
151 |
+
try {
|
152 |
+
const data = await apiRequest('/login', 'POST', { email, password });
|
153 |
+
if (data.access_token) {
|
154 |
+
authToken = data.access_token;
|
155 |
+
localStorage.setItem('authToken', authToken); // Store token
|
156 |
+
setStatus(loginStatus, ""); // Clear status
|
157 |
+
await showWelcomePage(); // Fetch user info and show welcome
|
158 |
+
} else {
|
159 |
+
throw new Error("Login failed: No token received.");
|
160 |
+
}
|
161 |
+
} catch (error) {
|
162 |
+
setStatus(loginStatus, `Login failed: ${error.message}`);
|
163 |
+
}
|
164 |
+
}
|
165 |
+
|
166 |
+
async function handleRegister(email, password) {
|
167 |
+
setStatus(registerStatus, "Registering...");
|
168 |
+
try {
|
169 |
+
const data = await apiRequest('/register', 'POST', { email, password });
|
170 |
+
setStatus(registerStatus, `Registration successful for ${data.email}! Please log in.`, true);
|
171 |
+
registerForm.reset(); // Clear form
|
172 |
+
showSection('login-section'); // Switch to login
|
173 |
+
} catch (error) {
|
174 |
+
setStatus(registerStatus, `Registration failed: ${error.message}`);
|
175 |
+
}
|
176 |
+
}
|
177 |
+
|
178 |
+
async function showWelcomePage() {
|
179 |
+
if (!authToken) {
|
180 |
+
showSection('login-section');
|
181 |
+
return;
|
182 |
+
}
|
183 |
+
try {
|
184 |
+
// Fetch user details using the token
|
185 |
+
const user = await apiRequest('/users/me', 'GET', null, authToken);
|
186 |
+
welcomeMessage.textContent = `Welcome, ${user.email}!`;
|
187 |
+
showSection('welcome-section');
|
188 |
+
connectWebSocket(authToken); // Connect WS after successful login and user fetch
|
189 |
+
} catch (error) {
|
190 |
+
// If fetching user fails (e.g., invalid/expired token), logout
|
191 |
+
console.error("Failed to fetch user details:", error);
|
192 |
+
handleLogout();
|
193 |
+
}
|
194 |
+
}
|
195 |
+
|
196 |
+
function handleLogout() {
|
197 |
+
authToken = null;
|
198 |
+
localStorage.removeItem('authToken');
|
199 |
+
disconnectWebSocket();
|
200 |
+
setStatus(loginStatus, ""); // Clear any previous login errors
|
201 |
+
showSection('login-section');
|
202 |
+
}
|
203 |
+
|
204 |
+
|
205 |
+
// --- Event Listeners ---
|
206 |
+
registerForm.addEventListener('submit', (e) => {
|
207 |
+
e.preventDefault();
|
208 |
+
const email = document.getElementById('reg-email').value;
|
209 |
+
const password = document.getElementById('reg-password').value;
|
210 |
+
const confirmPassword = document.getElementById('reg-confirm-password').value;
|
211 |
+
if (password !== confirmPassword) {
|
212 |
+
setStatus(registerStatus, "Passwords do not match.");
|
213 |
+
return;
|
214 |
+
}
|
215 |
+
if (password.length < 8) {
|
216 |
+
setStatus(registerStatus, "Password must be at least 8 characters.");
|
217 |
+
return;
|
218 |
+
}
|
219 |
+
handleRegister(email, password);
|
220 |
+
});
|
221 |
+
|
222 |
+
loginForm.addEventListener('submit', (e) => {
|
223 |
+
e.preventDefault();
|
224 |
+
const email = document.getElementById('login-email').value;
|
225 |
+
const password = document.getElementById('login-password').value;
|
226 |
+
handleLogin(email, password);
|
227 |
+
});
|
228 |
+
|
229 |
+
logoutButton.addEventListener('click', handleLogout);
|
230 |
+
|
231 |
+
|
232 |
+
// --- Initial Page Load Logic ---
|
233 |
+
document.addEventListener('DOMContentLoaded', () => {
|
234 |
+
if (authToken) {
|
235 |
+
console.log("Token found, attempting to show welcome page.");
|
236 |
+
showWelcomePage(); // Try to fetch user info and show welcome
|
237 |
+
} else {
|
238 |
+
console.log("No token found, showing login page.");
|
239 |
+
showSection('login-section'); // Show login if no token
|
240 |
+
}
|
241 |
+
});
|
streamlit_app.py
DELETED
@@ -1,269 +0,0 @@
|
|
1 |
-
# streamlit_app.py
|
2 |
-
import streamlit as st
|
3 |
-
from streamlit_autorefresh import st_autorefresh # For periodic refresh
|
4 |
-
import httpx
|
5 |
-
import asyncio
|
6 |
-
import websockets
|
7 |
-
import json
|
8 |
-
import threading
|
9 |
-
import queue
|
10 |
-
import logging
|
11 |
-
import time
|
12 |
-
import os
|
13 |
-
|
14 |
-
# Import backend components
|
15 |
-
from app import crud, models, schemas, auth, dependencies
|
16 |
-
from app.database import ensure_db_and_table_exist # Sync function
|
17 |
-
from app.websocket import manager # Import the manager instance
|
18 |
-
|
19 |
-
# FastAPI imports for mounting
|
20 |
-
from fastapi import FastAPI, Depends, HTTPException, status
|
21 |
-
from fastapi.routing import Mount
|
22 |
-
from fastapi.staticfiles import StaticFiles
|
23 |
-
from app.api import router as api_router # Import the specific API router
|
24 |
-
|
25 |
-
# --- Logging ---
|
26 |
-
logging.basicConfig(level=logging.INFO)
|
27 |
-
logger = logging.getLogger(__name__)
|
28 |
-
|
29 |
-
# --- Configuration ---
|
30 |
-
# Use environment variable or default for local vs deployed API endpoint
|
31 |
-
# Since we are mounting FastAPI within Streamlit for the HF Space deployment:
|
32 |
-
API_BASE_URL = "http://127.0.0.1:7860/api" # Calls within the same process
|
33 |
-
WS_BASE_URL = API_BASE_URL.replace("http", "ws")
|
34 |
-
|
35 |
-
# --- Ensure DB exists on first run ---
|
36 |
-
# This runs once per session/process start
|
37 |
-
ensure_db_and_table_exist()
|
38 |
-
|
39 |
-
# --- FastAPI Mounting Setup ---
|
40 |
-
# Create a FastAPI instance (separate from the Streamlit one)
|
41 |
-
# We won't run this directly with uvicorn, but Streamlit uses it internally
|
42 |
-
api_app = FastAPI(title="Backend API") # Can add lifespan if needed for API-specific setup later
|
43 |
-
api_app.include_router(api_router, prefix="/api")
|
44 |
-
|
45 |
-
# Mount the FastAPI app within Streamlit's internal Tornado server
|
46 |
-
# This requires monkey-patching or using available hooks if Streamlit allows.
|
47 |
-
# Simpler approach for HF Space: Run FastAPI separately is cleaner if possible.
|
48 |
-
# Reverting to the idea that for this HF Space demo, API calls will be internal HTTP requests.
|
49 |
-
|
50 |
-
# --- WebSocket Listener Thread ---
|
51 |
-
stop_event = threading.Event()
|
52 |
-
notification_queue = queue.Queue()
|
53 |
-
|
54 |
-
def websocket_listener(token: str):
|
55 |
-
"""Runs in a background thread to listen for WebSocket messages."""
|
56 |
-
logger.info(f"[WS Thread] Listener started for token: {token[:10]}...")
|
57 |
-
ws_url = f"{WS_BASE_URL}/ws/{token}"
|
58 |
-
|
59 |
-
async def listen():
|
60 |
-
try:
|
61 |
-
async with websockets.connect(ws_url, open_timeout=10.0) as ws:
|
62 |
-
logger.info(f"[WS Thread] Connected to {ws_url}")
|
63 |
-
st.session_state['ws_connected'] = True
|
64 |
-
while not stop_event.is_set():
|
65 |
-
try:
|
66 |
-
message = await asyncio.wait_for(ws.recv(), timeout=1.0) # Check stop_event frequently
|
67 |
-
logger.info(f"[WS Thread] Received message: {message[:100]}...")
|
68 |
-
try:
|
69 |
-
data = json.loads(message)
|
70 |
-
if data.get("type") == "new_user":
|
71 |
-
notification = schemas.Notification(**data)
|
72 |
-
notification_queue.put(notification.message) # Put message in queue
|
73 |
-
logger.info("[WS Thread] Put notification in queue.")
|
74 |
-
except json.JSONDecodeError:
|
75 |
-
logger.error("[WS Thread] Failed to decode JSON.")
|
76 |
-
except Exception as e:
|
77 |
-
logger.error(f"[WS Thread] Error processing message: {e}")
|
78 |
-
|
79 |
-
except asyncio.TimeoutError:
|
80 |
-
continue # No message, check stop_event again
|
81 |
-
except websockets.ConnectionClosed:
|
82 |
-
logger.warning("[WS Thread] Connection closed.")
|
83 |
-
break # Exit loop if closed
|
84 |
-
except Exception as e:
|
85 |
-
logger.error(f"[WS Thread] Connection failed or error: {e}")
|
86 |
-
finally:
|
87 |
-
logger.info("[WS Thread] Listener loop finished.")
|
88 |
-
st.session_state['ws_connected'] = False
|
89 |
-
|
90 |
-
try:
|
91 |
-
asyncio.run(listen())
|
92 |
-
except Exception as e:
|
93 |
-
logger.error(f"[WS Thread] asyncio.run error: {e}")
|
94 |
-
logger.info("[WS Thread] Listener thread exiting.")
|
95 |
-
|
96 |
-
|
97 |
-
# --- Streamlit UI ---
|
98 |
-
|
99 |
-
st.set_page_config(layout="wide")
|
100 |
-
|
101 |
-
# --- Initialize Session State ---
|
102 |
-
if 'logged_in' not in st.session_state:
|
103 |
-
st.session_state.logged_in = False
|
104 |
-
if 'token' not in st.session_state:
|
105 |
-
st.session_state.token = None
|
106 |
-
if 'user_email' not in st.session_state:
|
107 |
-
st.session_state.user_email = None
|
108 |
-
if 'notifications' not in st.session_state:
|
109 |
-
st.session_state.notifications = []
|
110 |
-
if 'ws_thread' not in st.session_state:
|
111 |
-
st.session_state.ws_thread = None
|
112 |
-
if 'ws_connected' not in st.session_state:
|
113 |
-
st.session_state.ws_connected = False
|
114 |
-
|
115 |
-
# --- Notification Processing ---
|
116 |
-
new_notifications = []
|
117 |
-
while not notification_queue.empty():
|
118 |
-
try:
|
119 |
-
msg = notification_queue.get_nowait()
|
120 |
-
new_notifications.append(msg)
|
121 |
-
except queue.Empty:
|
122 |
-
break
|
123 |
-
|
124 |
-
if new_notifications:
|
125 |
-
logger.info(f"Processing {len(new_notifications)} notifications from queue.")
|
126 |
-
# Prepend new notifications to the session state list
|
127 |
-
current_list = st.session_state.notifications
|
128 |
-
st.session_state.notifications = new_notifications + current_list
|
129 |
-
# Limit history
|
130 |
-
if len(st.session_state.notifications) > 15:
|
131 |
-
st.session_state.notifications = st.session_state.notifications[:15]
|
132 |
-
# No explicit rerun needed here, Streamlit should rerun due to state change (?)
|
133 |
-
# or due to autorefresh below.
|
134 |
-
|
135 |
-
# --- Auto Refresh ---
|
136 |
-
# Refresh every 2 seconds to check the queue and update display
|
137 |
-
count = st_autorefresh(interval=2000, limit=None, key="notifrefresh")
|
138 |
-
|
139 |
-
# --- API Client ---
|
140 |
-
client = httpx.AsyncClient(base_url=API_BASE_URL, timeout=10.0)
|
141 |
-
|
142 |
-
# --- Helper Functions for API Calls ---
|
143 |
-
async def api_register(email, password):
|
144 |
-
try:
|
145 |
-
response = await client.post("/register", json={"email": email, "password": password})
|
146 |
-
response.raise_for_status()
|
147 |
-
return {"success": True, "data": response.json()}
|
148 |
-
except httpx.HTTPStatusError as e:
|
149 |
-
detail = e.response.json().get("detail", e.response.text)
|
150 |
-
logger.error(f"API Register Error: {e.response.status_code} - {detail}")
|
151 |
-
return {"success": False, "error": f"API Error: {detail}"}
|
152 |
-
except Exception as e:
|
153 |
-
logger.exception("Register call failed")
|
154 |
-
return {"success": False, "error": f"Request failed: {e}"}
|
155 |
-
|
156 |
-
async def api_login(email, password):
|
157 |
-
try:
|
158 |
-
response = await client.post("/login", json={"email": email, "password": password})
|
159 |
-
response.raise_for_status()
|
160 |
-
return {"success": True, "data": response.json()}
|
161 |
-
except httpx.HTTPStatusError as e:
|
162 |
-
detail = e.response.json().get("detail", e.response.text)
|
163 |
-
logger.error(f"API Login Error: {e.response.status_code} - {detail}")
|
164 |
-
return {"success": False, "error": f"API Error: {detail}"}
|
165 |
-
except Exception as e:
|
166 |
-
logger.exception("Login call failed")
|
167 |
-
return {"success": False, "error": f"Request failed: {e}"}
|
168 |
-
|
169 |
-
# --- UI Rendering ---
|
170 |
-
st.title("Authentication & Notification App (Streamlit)")
|
171 |
-
|
172 |
-
if not st.session_state.logged_in:
|
173 |
-
st.sidebar.header("Login or Register")
|
174 |
-
login_tab, register_tab = st.sidebar.tabs(["Login", "Register"])
|
175 |
-
|
176 |
-
with login_tab:
|
177 |
-
with st.form("login_form"):
|
178 |
-
login_email = st.text_input("Email", key="login_email")
|
179 |
-
login_password = st.text_input("Password", type="password", key="login_password")
|
180 |
-
login_button = st.form_submit_button("Login")
|
181 |
-
|
182 |
-
if login_button:
|
183 |
-
if not login_email or not login_password:
|
184 |
-
st.error("Please enter email and password.")
|
185 |
-
else:
|
186 |
-
result = asyncio.run(api_login(login_email, login_password)) # Run async in sync context
|
187 |
-
if result["success"]:
|
188 |
-
token = result["data"]["access_token"]
|
189 |
-
# Attempt to get user info immediately - needs modification if /users/me requires auth header
|
190 |
-
# For simplicity, just store email from login form for now
|
191 |
-
st.session_state.logged_in = True
|
192 |
-
st.session_state.token = token
|
193 |
-
st.session_state.user_email = login_email # Store email used for login
|
194 |
-
st.session_state.notifications = [] # Clear old notifications
|
195 |
-
|
196 |
-
# Start WebSocket listener thread
|
197 |
-
stop_event.clear() # Ensure stop event is clear
|
198 |
-
thread = threading.Thread(target=websocket_listener, args=(token,), daemon=True)
|
199 |
-
st.session_state.ws_thread = thread
|
200 |
-
thread.start()
|
201 |
-
logger.info("Login successful, WS thread started.")
|
202 |
-
st.rerun() # Rerun immediately to switch view
|
203 |
-
else:
|
204 |
-
st.error(f"Login failed: {result['error']}")
|
205 |
-
|
206 |
-
with register_tab:
|
207 |
-
with st.form("register_form"):
|
208 |
-
reg_email = st.text_input("Email", key="reg_email")
|
209 |
-
reg_password = st.text_input("Password", type="password", key="reg_password")
|
210 |
-
reg_confirm = st.text_input("Confirm Password", type="password", key="reg_confirm")
|
211 |
-
register_button = st.form_submit_button("Register")
|
212 |
-
|
213 |
-
if register_button:
|
214 |
-
if not reg_email or not reg_password or not reg_confirm:
|
215 |
-
st.error("Please fill all fields.")
|
216 |
-
elif reg_password != reg_confirm:
|
217 |
-
st.error("Passwords do not match.")
|
218 |
-
elif len(reg_password) < 8:
|
219 |
-
st.error("Password must be at least 8 characters.")
|
220 |
-
else:
|
221 |
-
result = asyncio.run(api_register(reg_email, reg_password))
|
222 |
-
if result["success"]:
|
223 |
-
st.success(f"Registration successful for {result['data']['email']}! Please log in.")
|
224 |
-
else:
|
225 |
-
st.error(f"Registration failed: {result['error']}")
|
226 |
-
|
227 |
-
else: # Logged In View
|
228 |
-
st.sidebar.header(f"Welcome, {st.session_state.user_email}!")
|
229 |
-
if st.sidebar.button("Logout"):
|
230 |
-
logger.info("Logout requested.")
|
231 |
-
# Stop WebSocket thread
|
232 |
-
if st.session_state.ws_thread and st.session_state.ws_thread.is_alive():
|
233 |
-
logger.info("Signalling WS thread to stop.")
|
234 |
-
stop_event.set()
|
235 |
-
st.session_state.ws_thread.join(timeout=2.0) # Wait briefly for thread exit
|
236 |
-
if st.session_state.ws_thread.is_alive():
|
237 |
-
logger.warning("WS thread did not exit cleanly.")
|
238 |
-
# Clear session state
|
239 |
-
st.session_state.logged_in = False
|
240 |
-
st.session_state.token = None
|
241 |
-
st.session_state.user_email = None
|
242 |
-
st.session_state.notifications = []
|
243 |
-
st.session_state.ws_thread = None
|
244 |
-
st.session_state.ws_connected = False
|
245 |
-
logger.info("Session cleared.")
|
246 |
-
st.rerun()
|
247 |
-
|
248 |
-
st.header("Dashboard")
|
249 |
-
# Display notifications
|
250 |
-
st.subheader("Real-time Notifications")
|
251 |
-
ws_status = "Connected" if st.session_state.ws_connected else "Disconnected"
|
252 |
-
st.caption(f"WebSocket Status: {ws_status}")
|
253 |
-
|
254 |
-
if st.session_state.notifications:
|
255 |
-
for i, msg in enumerate(st.session_state.notifications):
|
256 |
-
st.info(f"{msg}", icon="🔔")
|
257 |
-
else:
|
258 |
-
st.text("No new notifications.")
|
259 |
-
|
260 |
-
# Add a button to manually check queue/refresh if needed
|
261 |
-
# if st.button("Check for notifications"):
|
262 |
-
# st.rerun() # Force rerun which includes queue check
|
263 |
-
|
264 |
-
|
265 |
-
# --- Final Cleanup ---
|
266 |
-
# Ensure httpx client is closed if script exits abnormally
|
267 |
-
# (This might not always run depending on how Streamlit terminates)
|
268 |
-
# Ideally handled within context managers if used more extensively
|
269 |
-
# asyncio.run(client.aclose())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/index.html
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>Auth App</title>
|
7 |
+
<link rel="stylesheet" href="/static/css/style.css">
|
8 |
+
</head>
|
9 |
+
<body>
|
10 |
+
<h1>Auth & Notification App</h1>
|
11 |
+
|
12 |
+
<!-- Registration Section -->
|
13 |
+
<div id="register-section" class="auth-section">
|
14 |
+
<h2>Register</h2>
|
15 |
+
<form id="register-form">
|
16 |
+
<label for="reg-email">Email:</label>
|
17 |
+
<input type="email" id="reg-email" name="email" required><br><br>
|
18 |
+
<label for="reg-password">Password:</label>
|
19 |
+
<input type="password" id="reg-password" name="password" required minlength="8"><br><br>
|
20 |
+
<label for="reg-confirm-password">Confirm Password:</label>
|
21 |
+
<input type="password" id="reg-confirm-password" name="confirm_password" required minlength="8"><br><br>
|
22 |
+
<button type="submit">Register</button>
|
23 |
+
</form>
|
24 |
+
<p id="register-status" class="status-message"></p>
|
25 |
+
<p>Already have an account? <a href="#" onclick="showSection('login-section')">Login here</a></p>
|
26 |
+
</div>
|
27 |
+
|
28 |
+
<!-- Login Section -->
|
29 |
+
<div id="login-section" class="auth-section" style="display: none;">
|
30 |
+
<h2>Login</h2>
|
31 |
+
<form id="login-form">
|
32 |
+
<label for="login-email">Email:</label>
|
33 |
+
<input type="email" id="login-email" name="email" required><br><br>
|
34 |
+
<label for="login-password">Password:</label>
|
35 |
+
<input type="password" id="login-password" name="password" required><br><br>
|
36 |
+
<button type="submit">Login</button>
|
37 |
+
</form>
|
38 |
+
<p id="login-status" class="status-message"></p>
|
39 |
+
<p>Don't have an account? <a href="#" onclick="showSection('register-section')">Register here</a></p>
|
40 |
+
</div>
|
41 |
+
|
42 |
+
<!-- Welcome Section (shown after login) -->
|
43 |
+
<div id="welcome-section" style="display: none;">
|
44 |
+
<h2>Welcome!</h2>
|
45 |
+
<p id="welcome-message">Welcome, user!</p>
|
46 |
+
<button id="logout-button">Logout</button>
|
47 |
+
<hr>
|
48 |
+
<h2>Real-time Notifications</h2>
|
49 |
+
<div id="notifications">
|
50 |
+
<p><em>Notifications will appear here...</em></p>
|
51 |
+
</div>
|
52 |
+
</div>
|
53 |
+
|
54 |
+
<script src="/static/js/script.js"></script>
|
55 |
+
</body>
|
56 |
+
</html>
|