diff --git a/README.md b/README.md index 480cbff..f08ca8a 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,23 @@ Streamlit 앱 실행: streamlit run src/app.py --server.port=20000 ``` +### 인증 설정 + +애플리케이션은 streamlit-authenticator를 사용하여 로그인 기능을 제공합니다. 기본 계정은 다음과 같습니다: + +- 관리자: 아이디 `admin`, 비밀번호 `adminpass` +- 일반 사용자: 아이디 `user`, 비밀번호 `userpass` + +비밀번호 변경 또는 계정 추가를 위한 steps: + +1. `src/config/generate_credentials.py` 파일을 수정하여 원하는 계정 정보 입력 +2. 아래 명령어 실행: + ``` + cd src/config + python generate_credentials.py + ``` +3. 생성된 `credentials.yaml` 파일을 확인하여 적용 + ## 모듈 ### 데이터 수집 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..eace486 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +streamlit +pandas +pymysql +sqlalchemy +python-dotenv +PyYAML +streamlit-authenticator>=0.2.2 \ No newline at end of file diff --git a/src/app.py b/src/app.py index 56780af..4b140ae 100644 --- a/src/app.py +++ b/src/app.py @@ -1,7 +1,11 @@ """ Main Streamlit application for the Quant Manager. """ +import os +import yaml import streamlit as st +import streamlit_authenticator as stauth +from yaml.loader import SafeLoader from ui.pages.data_page import render_data_page from ui.pages.multi_factor_page import render_multi_factor_page from ui.pages.quality_page import render_quality_page @@ -18,29 +22,69 @@ st.set_page_config( # Define the sidebar navigation def main(): """Main application function.""" - # Create sidebar navigation - st.sidebar.title("콴트 매니저") + # Load authentication configuration + config_path = os.path.join(os.path.dirname(__file__), 'config', 'credentials.yaml') - # Navigation options - pages = { - "데이터 수집": render_data_page, - "멀티 팩터 전략": render_multi_factor_page, - "슈퍼 퀄리티 전략": render_quality_page, - "슈퍼 밸류 모멘텀 전략": render_value_momentum_page - } - - # Select page - selection = st.sidebar.radio("메뉴", list(pages.keys())) - - # Render the selected page - pages[selection]() - - # Footer - st.sidebar.markdown("---") - st.sidebar.info( - "© 2023-2025 콴트 매니저\n\n" - "한국 주식 시장을 위한 퀀트 투자 도구" - ) + if os.path.exists(config_path): + with open(config_path) as file: + config = yaml.load(file, Loader=SafeLoader) + + # Create authenticator object + authenticator = stauth.Authenticate( + config['credentials'], + config['cookie']['name'], + config['cookie']['key'], + config['cookie']['expiry_days'], + ) + + # Initialize variables + name = None + + # Create login widget with error handling + try: + authenticator.login() + except Exception as e: + st.error(f"로그인 처리 중 오류가 발생했습니다: {str(e)}") + st.info("authenticator.login() 호출 문제가 발생했습니다. 구성 파일을 확인하세요.") + + # Handle authentication status + if st.session_state['authentication_status'] == False: + st.error('아이디 또는 비밀번호가 올바르지 않습니다.') + + if st.session_state['authentication_status'] is None: + st.warning('로그인이 필요합니다.') + + # Display application if authenticated + if st.session_state['authentication_status']: + # Create sidebar navigation + st.sidebar.title(f"콴트 매니저 - {st.session_state["name"]}") + + # Add logout button + authenticator.logout('로그아웃', 'sidebar') + + # Navigation options + pages = { + "데이터 수집": render_data_page, + "멀티 팩터 전략": render_multi_factor_page, + "슈퍼 퀄리티 전략": render_quality_page, + "슈퍼 밸류 모멘텀 전략": render_value_momentum_page + } + + # Select page + selection = st.sidebar.radio("메뉴", list(pages.keys())) + + # Render the selected page + pages[selection]() + + # Footer + st.sidebar.markdown("---") + st.sidebar.info( + "© 2023-2025 콴트 매니저\n\n" + "한국 주식 시장을 위한 퀀트 투자 도구" + ) + else: + st.error("인증 설정 파일을 찾을 수 없습니다. config/credentials.yaml 파일을 확인하세요.") + st.info(f"찾을 파일 경로: {config_path}") if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/config/credentials.yaml b/src/config/credentials.yaml new file mode 100644 index 0000000..7e6feb8 --- /dev/null +++ b/src/config/credentials.yaml @@ -0,0 +1,10 @@ +cookie: + expiry_days: 30 + key: 37A93A1A98AC9E896CBB21DD1F84FABC + name: quant_manager_auth +credentials: + usernames: + zephyrdark: + email: zephyrdark@gamil.com + name: ayuriel + password: $2b$12$nqVU.Mw8lErQU59TxuGWLebfImar1O9zirjpc.waN/3LNYzc2o4.6 diff --git a/src/config/generate_credentials.py b/src/config/generate_credentials.py new file mode 100644 index 0000000..b15ecf2 --- /dev/null +++ b/src/config/generate_credentials.py @@ -0,0 +1,51 @@ +""" +Generate a credentials.yaml file with hashed passwords for streamlit-authenticator. +This script should be run once to set up authentication credentials. +""" +import yaml +import streamlit_authenticator as stauth +from yaml.loader import SafeLoader + +# Create credentials +usernames = ["zephyrdark"] +names = ["ayuriel"] +passwords = [""] # Replace with secure passwords + +# Hash passwords +try: + # For newer versions of streamlit-authenticator + hashed_passwords = [stauth.Hasher([passwords[0]]).generate()[0]] +except: + # For older versions of streamlit-authenticator + hashed_passwords = [stauth.Hasher().hash(passwords[0])] + +# Create config dictionary +config = { + 'credentials': { + 'usernames': {} + }, + 'cookie': { + 'name': 'quant_manager_auth', + 'key': '37A93A1A98AC9E896CBB21DD1F84FABC', # Key should be a string that is 32 bytes long + 'expiry_days': 30 + }, + 'preauthorized': { + 'emails': [] + } +} + +# Add user credentials +for i, username in enumerate(usernames): + config['credentials']['usernames'][username] = { + 'name': names[i], + 'password': hashed_passwords[i], + 'email': f"{username}@gamil.com" # Replace with actual emails if needed + } + +# Write to YAML file +with open('credentials.yaml', 'w') as file: + yaml.dump(config, file, default_flow_style=False) + +print("Credentials file has been generated successfully!") +print("Update the usernames, passwords, and other information as needed.") +print("Then move the credentials.yaml file to the config directory.") \ No newline at end of file diff --git a/src/config/update_password.py b/src/config/update_password.py new file mode 100644 index 0000000..159de7e --- /dev/null +++ b/src/config/update_password.py @@ -0,0 +1,75 @@ +""" +Update password for an existing user in credentials.yaml. +""" +import os +import yaml +import getpass +import streamlit_authenticator as stauth +from yaml.loader import SafeLoader + +def main(): + # Get the path to credentials.yaml + script_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(script_dir, 'credentials.yaml') + + # Check if credentials file exists + if not os.path.exists(config_path): + print(f"Error: {config_path} not found.") + return + + # Load the credentials file + with open(config_path, 'r') as file: + config = yaml.load(file, Loader=SafeLoader) + + # Get all usernames + usernames = list(config['credentials']['usernames'].keys()) + + if not usernames: + print("No users found in credentials file.") + return + + # Print available users + print("Available users:") + for i, username in enumerate(usernames, 1): + print(f"{i}. {username}") + + try: + # Get user selection + choice = int(input("\nSelect user number to update password: ")) + if choice < 1 or choice > len(usernames): + print("Invalid selection.") + return + + selected_username = usernames[choice - 1] + + # Get new password + new_password = getpass.getpass("Enter new password: ") + confirm_password = getpass.getpass("Confirm new password: ") + + if new_password != confirm_password: + print("Passwords do not match.") + return + + if not new_password: + print("Password cannot be empty.") + return + + # Hash the new password + hashed_password = stauth.Hasher([new_password]).generate()[0] + + # Update the password + config['credentials']['usernames'][selected_username]['password'] = hashed_password + + # Save the updated config + with open(config_path, 'w') as file: + yaml.dump(config, file, default_flow_style=False) + + print(f"Password updated successfully for {selected_username}.") + + except (ValueError, IndexError): + print("Invalid input.") + except Exception as e: + print(f"An error occurred: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file