Skip to content

Commit

Permalink
backup: 2024-10-22
Browse files Browse the repository at this point in the history
  • Loading branch information
minsubak committed Oct 22, 2024
1 parent 79b04e5 commit ba4e166
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 80 deletions.
29 changes: 14 additions & 15 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ services:
react:
build:
context: ./react
hostname: react
container_name: react
hostname: react_
container_name: react_
ports:
- "3000:3000"
environment:
Expand All @@ -18,31 +18,30 @@ services:
fastapi:
build:
context: ./fastapi
hostname: fastapi
container_name: fastapi
ports:
- "8000:8000"
# volumes:
# - .:/app
hostname: fastapi_
container_name: fastapi_
volumes:
- ./fastapi/app:/app
environment:
- TZ=Asia/Seoul
- ENVIRONMENT=development
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
restart: always
network_mode: "host"
# ports: # IoT 장비와 통신 오류 발생 (도커 네트워크와 호환성 문제 예상)
# - "8000:8000"

# db
mysql:
build:
context: ./mysql
container_name: mysql
ports:
- "3306:3306"
hostname: mysql
hostname: mysql_
container_name: mysql_
privileged: true
environment:
- TZ=Asia/Seoul
# volumes:
# - ./init.sql:/docker-entrypoint-initdb.d/init.sql
- TZ=Asia/Seoul
restart: always
ports:
- "3306:3306"

# DB와 WAS는 volume을 따로 할당하여 배포 과정 도중 고객 데이터 손실 방지
60 changes: 41 additions & 19 deletions fastapi/app/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,49 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "mysql+asyncmy://root:1234@mysql:3306/iot_device_db"
DATABASE_URLS = [
"mysql+asyncmy://root:1234@mysql:3306/iot_device_db", # docker network bridge
"mysql+asyncmy://root:1234@localhost:3306/iot_device_db" # docker host network
]

engine = create_async_engine(DATABASE_URL, echo=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
engines = [create_async_engine(url, echo=True) for url in DATABASE_URLS]
SessionLocals = [
sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
for engine in engines
]
Base = declarative_base()

async def init_db(delay=5):
while True:
try:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
print("INFO: Database connection successful and table creation complete.", flush=True)
break
except Exception as e:
print(f"INFO: Database connection failure. \n{e}", flush=True)
print(f"INFO: Retrying in {delay} seconds...", flush=True)
await asyncio.sleep(delay)
async def init_db(delay=5, retries=12): # 1 min
for n in range(retries):
tasks = []
for engine in engines:
tasks.append(init_connection(engine))

results = await asyncio.gather(*tasks, return_exceptions=True)

for idx, result in enumerate(results):
if isinstance(result, Exception):
print(f"INFO: Connection failed to {DATABASE_URLS[idx]}. ({n + 1}) \n{result}", flush=True)
else:
print(f"INFO: Connected to {DATABASE_URLS[idx]} and tables created.", flush=True)
return

print(f"INFO: Retrying all connections in {delay} seconds...", flush=True)
await asyncio.sleep(delay)

print("ERROR: All connection attempts failed.", flush=True)
exit(1)

async def init_connection(engine):
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

async def get_db():
async with SessionLocal() as db:
try:
yield db
finally:
await db.close()
"""둘 중 하나의 데이터베이스에 연결하여 세션을 반환."""
for SessionLocal in SessionLocals:
async with SessionLocal() as db:
try:
yield db
return
except Exception as e:
print(f"WARNING: Failed to get DB session. {e}", flush=True)
94 changes: 69 additions & 25 deletions fastapi/app/main.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
from fastapi import FastAPI, Request, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from contextlib import asynccontextmanager
from typing import Optional
from typing import Optional, Dict
from database import *
from method import *
from func import *

import socketio

# Initalize SocketIO server with ASGI
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
sio = socketio.AsyncServer(
async_mode="asgi",
cors_allowed_origins="*",
transports=["websocket", "polling"],
logger=True,
engineio_logger=True
)
socket_app = socketio.ASGIApp(sio ,socketio_path="/")

# Initialize FastAPI app with lifespan (database initialization)
Expand All @@ -22,17 +27,28 @@ async def lifespan(app: FastAPI):

app = FastAPI(lifespan=lifespan)

origins = [
"http://localhost:7000",
"http://mindou.pe.kr"
]

# CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost"],
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# OAuth2 Token Bearer for Authorization
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="access")
SECRET_KEY = "project_worthy"
ALGORITHM = "HS256"

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")

# Dictionary to track connected IoT devices
connected_iot_devices = {}

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # MySQL query transmission
# Search IoT device
Expand Down Expand Up @@ -65,38 +81,66 @@ async def delete_device_route(request: Request, device_id: Optional[str] = None,
return await remove_device(device_id, db)

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # External Server Transmission
# Login
@app.post("/access")
async def access_token_route(form_data: OAuth2PasswordRequestForm = Depends()):
return await access_token(form_data)

# Verify token
@app.get("/verify")
async def verify_token_route(token: str = Depends(OAuth2PasswordBearer(tokenUrl="token"))):
return await verify_token(token)
# 쿠키 또는 헤더에서 JWT 토큰 추출하는 함수
def get_token(request: Request) -> str:
token = request.cookies.get("access_token") # 쿠키에서 토큰 추출
if not token:
auth_header = request.headers.get("Authorization") # 헤더 확인
if auth_header and auth_header.startswith("Bearer "):
token = auth_header.split(" ", 1)[1] # 'Bearer ' 이후의 토큰 부분 추출
else:
print("INFO: Missing Token", flush=True)
raise HTTPException(status_code=401, detail="Missing token")
return token

# JWT 토큰 검증 함수
def verify_token(token: str) -> Dict:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
print("INFO: Invalid Token", flush=True)
raise HTTPException(status_code=401, detail="Invalid token")
return payload # 검증된 payload 반환
except JWTError:
print("INFO: Invalid or expired Token", flush=True)
raise HTTPException(status_code=401, detail="Invalid or expired token")

# /secure 엔드포인트: 인증된 사용자만 접근 가능
@app.get("/secure")
async def secure_route(request: Request):
token = get_token(request) # 쿠키 또는 헤더에서 JWT 추출
payload = verify_token(token) # 토큰 검증 및 디코딩
print(f"INFO: Payload: {payload}", flush=True)
return {"message": f"Hello {payload['sub']}, you have access to this route!"}

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # SocketIO server Transmission
# Dictionary to track connected IoT devices
connected_iot_devices = {}

# IoT device namespace handlers
@sio.event(namespace="/iot")
async def connect(sid, environ):
device_id = environ.get('HTTP_DEVICE_ID') # Get device ID from headers
connected_iot_devices[device_id] = sid # Track the device's socket ID
print(f"IoT Device {device_id} connected with SID {sid}.")
print(f"INFO: IoT Device {device_id} connected with SID {sid}.", flush=True)

# query_string = environ.get('QUERY_STRING', '')
# params = dict(param.split('=') for param in query_string.split('&'))
# device_id = params.get('device_id', f'unknown-{sid}')
# connected_iot_devices[device_id] = sid
# print(f"INFO: IoT Device({device_id}) connected with SID({sid}).")
# await sio.emit("ack", {"message": "Connection successful!"}, to=sid, namespace="/iot")
# print(f"INFO: IoT Device connected with SID({sid}).")

@sio.event(namespace="/iot")
async def disconnect(sid):
device_id = next((key for key, value in connected_iot_devices.items() if value == sid), None)
if device_id:
del connected_iot_devices[device_id]
print(f"IoT Device {device_id} disconnected.", flush=True)
print(f"INFO: IoT Device {device_id} disconnected.", flush=True)

@sio.on("iot_event", namespace="/iot")
async def iot_event(sid, data):
print(f"INFO: Received data from IoT Device {sid}: {data}", flush=True)
await sio.emit("iot_event", "Test", namespace="/iot")
await sio.emit("iot_event", "turn_off", namespace="/iot")

# Example of emitting data to all connected devices
# async def broadcast_to_all_devices(event, message):
Expand All @@ -106,15 +150,15 @@ async def iot_event(sid, data):
# AI Server Namespace ("/ai")
@sio.event(namespace="/ai")
async def connect_ai(sid, environ):
print(f"AI Server {sid} connected.", flush=True)
print(f"INFO: AI Server {sid} connected.", flush=True)

@sio.event(namespace="/ai")
async def disconnect_ai(sid):
print(f"AI Server {sid} disconnected.", flush=True)
print(f"INFO: AI Server {sid} disconnected.", flush=True)

@sio.on("ai_event", namespace="/ai")
async def ai_event(sid, data):
print(f"Received command from AI Server {sid}: {data}", flush=True)
print(f"INFO: Received command from AI Server {sid}: {data}", flush=True)

# Extract target IoT device and command from data
target_device_id = data.get("device_id")
Expand All @@ -126,9 +170,9 @@ async def ai_event(sid, data):

# Send command to the specified IoT device
await sio.emit("iot_command", {"command": command}, room=target_sid, namespace="/iot")
print(f"Sent command to IoT Device {target_device_id}: {command}", flush=True)
print(f"INFO: Sent command to IoT Device {target_device_id}: {command}", flush=True)
else:
print(f"IoT Device {target_device_id} is not connected.", flush=True)
print(f"INFO: IoT Device {target_device_id} is not connected.", flush=True)

# Mount SocketIO app at "/ws" for WebSocket communication
app.mount("/ws", socket_app)
1 change: 0 additions & 1 deletion fastapi/app/method/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@
from .update import *
from .remove import *
from .verify import *
from .access import *
from .schemas import *
16 changes: 0 additions & 16 deletions fastapi/app/method/access.py

This file was deleted.

11 changes: 7 additions & 4 deletions fastapi/app/method/verify.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from jose import JWTError
from login import *
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt

SECRET_KEY = "project_worthy"
ALGORITHM = "HS256"

async def verify_token(token: str = Depends(OAuth2PasswordBearer(tokenUrl="token"))):
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")

async def verify_token(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise HTTPException(status_code=401, detail="Invalid token")
return payload # payload를 반환하여 사용자 정보를 사용할 수 있도록 합니다.
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
return {"message": "Token is valid", "username": username}

0 comments on commit ba4e166

Please sign in to comment.