diff --git a/.github/workflows/build_release_and_notify.yml b/.github/workflows/build_release_and_notify.yml index 94f9306..7870623 100644 --- a/.github/workflows/build_release_and_notify.yml +++ b/.github/workflows/build_release_and_notify.yml @@ -75,6 +75,8 @@ jobs: # 6. PyInstaller를 사용해 파이썬 코드를 실행 파일로 만듭니다. - name: Build executable with PyInstaller shell: bash + env: + ENV_AES256_KEY: ${{ secrets.ENV_AES256_KEY }} run: poetry run pyinstaller --clean --additional-hooks-dir ./hooks --add-data "app/assets:assets" --onefile --name ${{ env.EXE_NAME }} app/main.py # 7. 빌드된 실행 파일을 다음 단계(deploy)에서 사용할 수 있도록 아티팩트로 업로드합니다. diff --git a/app/api/driver_api.py b/app/api/driver_api.py index 97a9e10..8d488fc 100644 --- a/app/api/driver_api.py +++ b/app/api/driver_api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter -from app.core.db_driver_enum import DBTypesEnum +from app.core.enum.db_driver import DBTypesEnum from app.core.exceptions import APIException from app.core.status import CommonCode from app.schemas.driver_info import DriverInfo -from app.schemas.response import ResponseMessage +from app.core.response import ResponseMessage from app.services.driver_service import db_driver_info router = APIRouter() diff --git a/app/api/test_api.py b/app/api/test_api.py index c3ae9c0..ca0dcca 100644 --- a/app/api/test_api.py +++ b/app/api/test_api.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.schemas.response import ResponseMessage +from app.core.response import ResponseMessage from app.core.exceptions import APIException from app.core.status import CommonCode diff --git a/app/core/db_driver_enum.py b/app/core/enum/db_driver.py similarity index 90% rename from app/core/db_driver_enum.py rename to app/core/enum/db_driver.py index 6896760..0ccf151 100644 --- a/app/core/db_driver_enum.py +++ b/app/core/enum/db_driver.py @@ -1,4 +1,4 @@ -# app/core/db_driver_enum.py +# app/core/enum/db_driver.py from enum import Enum diff --git a/app/schemas/response.py b/app/core/response.py similarity index 100% rename from app/schemas/response.py rename to app/core/response.py diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000..47376d7 --- /dev/null +++ b/app/core/security.py @@ -0,0 +1,44 @@ +import os +import base64 +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad +from Crypto.Random import get_random_bytes + +""" +보안 원칙을 적용한 AES-256 암호화 및 복호화 클래스입니다. +- 암호화 시 매번 새로운 랜덤 IV를 생성합니다. +""" +class AES256: + _key = base64.b64decode(os.getenv("ENV_AES256_KEY")) + + @staticmethod + def encrypt(text: str) -> str: + iv = get_random_bytes(AES.block_size) + + cipher = AES.new(AES256._key, AES.MODE_CBC, iv) + + data_bytes = text.encode('utf-8') + padded_bytes = pad(data_bytes, AES.block_size) + + encrypted_bytes = cipher.encrypt(padded_bytes) + + combined_bytes = iv + encrypted_bytes + return base64.b64encode(combined_bytes).decode('utf-8') + + @staticmethod + def decrypt(cipher_text: str) -> str: + """ + AES-256으로 암호화된 텍스트를 복호화합니다. + """ + combined_bytes = base64.b64decode(cipher_text) + + iv = combined_bytes[:AES.block_size] + encrypted_bytes = combined_bytes[AES.block_size:] + + cipher = AES.new(AES256._key, AES.MODE_CBC, iv) + + decrypted_padded_bytes = cipher.decrypt(encrypted_bytes) + decrypted_bytes = unpad(decrypted_padded_bytes, AES.block_size) + + return decrypted_bytes.decode('utf-8') + diff --git a/app/core/utils.py b/app/core/utils.py new file mode 100644 index 0000000..a4f7278 --- /dev/null +++ b/app/core/utils.py @@ -0,0 +1,23 @@ +import uuid +from pathlib import Path + +# 앱 데이터를 저장할 폴더 이름 +APP_DATA_DIR_NAME = ".qgenie" + +def get_db_path() -> Path: + """ + 사용자 홈 디렉터리 내에 앱 데이터 폴더를 만들고, + SQLite DB 파일의 전체 경로를 반환합니다. + """ + home_dir = Path.home() + app_data_dir = home_dir / APP_DATA_DIR_NAME + app_data_dir.mkdir(exist_ok=True) + db_path = app_data_dir / "local_storage.sqlite" + return db_path + +def generate_uuid() -> str: + return uuid.uuid4().hex.upper() + +def generate_prefixed_uuid(prefix: str) -> str: + return f"{prefix.upper()}-{uuid.uuid4().hex.upper()}" + diff --git a/app/db/init_db.py b/app/db/init_db.py new file mode 100644 index 0000000..4de856d --- /dev/null +++ b/app/db/init_db.py @@ -0,0 +1,130 @@ +# db/init_db.py +import sqlite3 +from app.core.utils import get_db_path + +""" +데이터베이스에 연결하고, 애플리케이션에 필요한 테이블이 없으면 생성합니다. +""" +def initialize_database(): + + db_path = get_db_path() + conn = None + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + # db_profile 테이블 생성 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS db_profile ( + id VARCHAR(64) PRIMARY KEY NOT NULL, + type VARCHAR(32) NOT NULL, + host VARCHAR(255) NOT NULL, + port INTEGER NOT NULL, + name VARCHAR(64), + username VARCHAR(128) NOT NULL, + password VARCHAR(128) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + """) + # db_profile 테이블의 updated_at을 자동으로 업데이트하는 트리거 + cursor.execute(""" + CREATE TRIGGER IF NOT EXISTS update_db_profile_updated_at + BEFORE UPDATE ON db_profile + FOR EACH ROW + BEGIN + UPDATE db_profile SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END; + """) + + # ai_credential 테이블 생성 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS ai_credential ( + id VARCHAR(64) PRIMARY KEY NOT NULL, + service_name VARCHAR(32) NOT NULL, + api_key VARCHAR(256) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + """) + # ai_credential 테이블의 updated_at을 자동으로 업데이트하는 트리거 + cursor.execute(""" + CREATE TRIGGER IF NOT EXISTS update_ai_credential_updated_at + BEFORE UPDATE ON ai_credential + FOR EACH ROW + BEGIN + UPDATE ai_credential SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END; + """) + + # chat_tab 테이블 생성 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS chat_tab ( + id VARCHAR(64) PRIMARY KEY NOT NULL, + name VARCHAR(255), + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + """) + # chat_tab 테이블의 updated_at을 자동으로 업데이트하는 트리거 + cursor.execute(""" + CREATE TRIGGER IF NOT EXISTS update_chat_tab_updated_at + BEFORE UPDATE ON chat_tab + FOR EACH ROW + BEGIN + UPDATE chat_tab SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END; + """) + + # chat_message 테이블 생성 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS chat_message ( + id VARCHAR(64) PRIMARY KEY NOT NULL, + chat_tab_id VARCHAR(64) NOT NULL, + sender VARCHAR(1) NOT NULL, + message TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (chat_tab_id) REFERENCES chat_tab(id) + ); + """) + # chat_message 테이블의 updated_at을 자동으로 업데이트하는 트리거 + cursor.execute(""" + CREATE TRIGGER IF NOT EXISTS update_chat_message_updated_at + BEFORE UPDATE ON chat_message + FOR EACH ROW + BEGIN + UPDATE chat_message SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END; + """) + + # query_history 테이블 생성 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS query_history ( + id VARCHAR(64) PRIMARY KEY NOT NULL, + chat_message_id VARCHAR(64) NOT NULL, + query_text TEXT NOT NULL, + is_success VARCHAR(1) NOT NULL, + error_message TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (chat_message_id) REFERENCES chat_message(id) + ); + """) + # query_history 테이블의 updated_at을 자동으로 업데이트하는 트리거 + cursor.execute(""" + CREATE TRIGGER IF NOT EXISTS update_query_history_updated_at + BEFORE UPDATE ON query_history + FOR EACH ROW + BEGIN + UPDATE query_history SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END; + """) + + conn.commit() + + except sqlite3.Error as e: + print(f"데이터베이스 초기화 중 오류 발생: {e}") + finally: + if conn: + conn.close() + diff --git a/app/main.py b/app/main.py index c3d6dd2..8a0f03b 100644 --- a/app/main.py +++ b/app/main.py @@ -6,6 +6,7 @@ from app.api import health # 헬스 체크 from app.api.api_router import api_router from app.core.exceptions import APIException, api_exception_handler, generic_exception_handler +from app.db.init_db import initialize_database app = FastAPI() @@ -17,6 +18,8 @@ app.include_router(health.router) app.include_router(api_router, prefix="/api") +# initialize_database 함수가 호출되어 테이블이 생성되거나 이미 존재함을 확인합니다. +initialize_database() if __name__ == "__main__": # Uvicorn 서버를 시작합니다. diff --git a/app/schemas/driver_info.py b/app/schemas/driver_info.py index f8e0b62..0d7c62d 100644 --- a/app/schemas/driver_info.py +++ b/app/schemas/driver_info.py @@ -1,7 +1,7 @@ # app/schemas/driver_info.py from pydantic import BaseModel -from app.core.db_driver_enum import DBTypesEnum +from app.core.enum.db_driver import DBTypesEnum class DriverInfo(BaseModel): diff --git a/poetry.lock b/poetry.lock index 1ad3eb7..ffe1686 100644 --- a/poetry.lock +++ b/poetry.lock @@ -739,6 +739,54 @@ files = [ {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"}, {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, +name = "pycryptodome" +version = "3.23.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +files = [ + {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, + {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, ] [[package]] @@ -1390,4 +1438,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.1" python-versions = ">=3.11" -content-hash = "b54ff38df6da37302c0517493eeedc366a91ccbb2f4ef7cec1185e0fd83b6e3e" +content-hash = "1426c31c951997738ac54c4653aa70807b59996be48822d9d9f59408c7f1a0bd" diff --git a/pyproject.toml b/pyproject.toml index fbc9141..f980df9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,8 @@ dependencies = [ "mysql-connector-python (>=9.4.0,<10.0.0)", "pymysql (>=1.1.1,<2.0.0)", "cx-oracle (>=8.3.0,<9.0.0)", - "pyodbc (>=5.2.0,<6.0.0)" + "pyodbc (>=5.2.0,<6.0.0)", + "pycryptodome (>=3.23.0,<4.0.0)" ]