From 421d263b6bb3ee247c895ec1abbfafefd7bf1123 Mon Sep 17 00:00:00 2001 From: Carl Zhang Date: Mon, 13 Oct 2025 23:50:53 -0500 Subject: [PATCH] add admin page to manage private data --- .gitignore | 1 + app.py | 8 +- webpages/__init__.py | 1 + webpages/admin.py | 34 +++++ webpages/adminpages/email_helloween_image.py | 68 ++++++++++ webpages/adminpages/helloween_image.py | 123 +++++++++++++++++++ webpages/buffteks_authenticator.py | 18 +++ webpages/navigation.py | 7 ++ webpages/users.yaml | 32 +++++ 9 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 webpages/admin.py create mode 100644 webpages/adminpages/email_helloween_image.py create mode 100644 webpages/adminpages/helloween_image.py create mode 100644 webpages/buffteks_authenticator.py create mode 100644 webpages/users.yaml diff --git a/.gitignore b/.gitignore index 52778a37e..114734176 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ files/student_locations.db __pycache__/ *.pyc *.cpython-312.pyc +venv/ \ No newline at end of file diff --git a/app.py b/app.py index b446f030e..aed583d9e 100644 --- a/app.py +++ b/app.py @@ -2,13 +2,12 @@ import streamlit as st import webpages as pg from webpages.navigation import navigation_bar as nv + st.set_page_config(page_title="BuffTeks Student Organization", page_icon="./images/BuffTeksLogo.png", layout = "centered" ) - - page_label = nv() # block for main pages @@ -64,4 +63,7 @@ elif page_label == "CoreTeks": elif page_label == "Testing": pg.testing() elif page_label == "Reference": - pg.reference() \ No newline at end of file + pg.reference() + +elif page_label == "Admin": + pg.admin() \ No newline at end of file diff --git a/webpages/__init__.py b/webpages/__init__.py index 4c0ea71af..bfdc49641 100644 --- a/webpages/__init__.py +++ b/webpages/__init__.py @@ -19,3 +19,4 @@ from .outstanding_members import outstanding_members from .cis_tech_challenge_pages.cis_tech_challenge_homepage import cis_tech_challenge_homepage from .coreteks_pages.coreteks_homepage import coreteks_homepage from .SendEmail import send_email +from .admin import admin \ No newline at end of file diff --git a/webpages/admin.py b/webpages/admin.py new file mode 100644 index 000000000..0f1a688f9 --- /dev/null +++ b/webpages/admin.py @@ -0,0 +1,34 @@ +import streamlit as st +import pandas as pd +import streamlit_authenticator as stauth + +from webpages.buffteks_authenticator import load_authenticator + +from webpages.adminpages.helloween_image import camera_photo + +def admin(): + + try: + authenticator = load_authenticator() + authenticator.login() + + except Exception as e: + st.error(e) + + + if st.session_state.get('authentication_status') is False: + st.error('Username/password is incorrect') + elif st.session_state.get('authentication_status') is None: + st.warning('Please enter your username and password') + + if st.session_state.get('authentication_status'): + authenticator.logout() + st.header(f'Welcome *{st.session_state.get("name")}*') + + st.divider() + with st.expander("Check Memberships"): + new_members = pd.read_json("new_members.json") + st.dataframe(new_members) + + st.divider() + camera_photo() diff --git a/webpages/adminpages/email_helloween_image.py b/webpages/adminpages/email_helloween_image.py new file mode 100644 index 000000000..6ed6b7492 --- /dev/null +++ b/webpages/adminpages/email_helloween_image.py @@ -0,0 +1,68 @@ +import smtplib +import io +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.mime.image import MIMEImage + +def send_email_with_attachment(sender_email, password, to_email, subject, image_bytes): + """ + Sends an email with an image attachment. + + Args: + sender_email (str): The sender's email address. + password (str): The sender's email password. + to_email (str): The recipient's email address. + subject (str): The subject of the email. + image_bytes (bytes): The image content as a byte string. + """ + message = MIMEMultipart('alternative') + message['Subject'] = subject + message['From'] = sender_email + message['To'] = to_email + + html_content = """ + + + Happy Halloween! + + +
+

๐ŸŽƒ Happy Halloween! ๐ŸŽƒ

+

+ Thank you for your interest in the + BuffTeks + student organization! +

+

+ Wishing you a spooktacular Halloween!
+ Here is your special holiday photo generated just for you.
+ Enjoy and have a frightfully fun day! +

+
+ + + """ + message.attach(MIMEText(html_content, 'html')) + + if image_bytes: + # The image object is converted to bytes before attaching. + img_byte_arr = io.BytesIO() + # Assuming image_bytes is a PIL Image object. + # Convert to RGB if necessary for JPEG format. + if image_bytes.mode in ('RGBA', 'P'): + image_bytes = image_bytes.convert('RGB') + image_bytes.save(img_byte_arr, format='JPEG') + img_byte_arr = img_byte_arr.getvalue() + + image_part = MIMEImage(img_byte_arr, _subtype="jpeg") + image_part.add_header('Content-Disposition', 'attachment', filename="halloween.jpg") + message.attach(image_part) + + try: + with smtplib.SMTP('mail.privateemail.com', 587) as server: + server.starttls() + server.login(sender_email, password) + server.send_message(message) + print("Email sent successfully!") + except Exception as e: + print(f"Failed to send email: {str(e)}") \ No newline at end of file diff --git a/webpages/adminpages/helloween_image.py b/webpages/adminpages/helloween_image.py new file mode 100644 index 000000000..1b2c53f9d --- /dev/null +++ b/webpages/adminpages/helloween_image.py @@ -0,0 +1,123 @@ +import streamlit as st +from PIL import Image +from google import genai +import json +from io import BytesIO +from .email_helloween_image import send_email_with_attachment + + +def camera_photo(): + st.markdown("

๐ŸŽƒ Halloween Photo Booth ๐Ÿ“ธ

", unsafe_allow_html=True) + st.markdown("
Capture your Halloween spirit with a festive photo! ๐Ÿ‘ป
", unsafe_allow_html=True) + + if 'image_response' not in st.session_state: + st.session_state.image_response = None + + enable_camera = st.checkbox("Enable Camera") + if enable_camera: + st.info("๐Ÿ“ŽNote: No photos are stored. The image is processed in-memory and deleted after refresh app.") + picture = st.camera_input("Take a Halloween-themed photo! ๐ŸŽƒ๐Ÿ‘ป๐Ÿ•ธ๏ธ") + + if picture is not None: + image = Image.open(picture) + + keep_dressing_style = st.checkbox("Keep original dressing style", value=False) + keep_body_pose = st.checkbox("Keep original body pose", value=False) + + theme = st.selectbox( + "Select or Input a Halloween theme for your photo:", + [ "๐ŸŽƒ Classic Halloween", + "๐Ÿ‘ป Haunted House", + "โš—๏ธ Science Experiment Gone Wrong", + "๐ŸŽฅ Horror Film Inspired", + "๐Ÿ•ฏ๏ธ Gothic", + "๐Ÿง™โ€โ™‚๏ธ Harry Potter", + "โšฐ๏ธ Graveyard", + "๐Ÿ”ฎ Cult Horror Theme" + ], + index=0, + key="theme_selector" + ) + + if st.button("AI Editing"): + edit_prompt = f""" + Retain original facial features (eyes, nose, mouth etc.) of any persons in the photo. The goal is to achieve a realistic, edited look, not an AI-generated or overly smoothed/perfected appearance. Avoid any artificial alterations to these features. + Keep the original dressing style: {str(keep_dressing_style)} + Keep the original body pose: {str(keep_body_pose)} + Theme: {theme} + If a person is already in costume, enhance the costume and add more Halloween elements and background to the scene. + If multiple persons are present, ensure all are included in the Halloween theme. + - Overall Style: The final image should be photorealistic. + Ensure the final image is vibrant, festive, and captures the Halloween spirit! + """ + with st.spinner("Editing image..."): + text_response, image_response = edit_image_with_ai(image, edit_prompt) + if text_response: + st.info(text_response) + if image_response: + st.session_state.image_response = image_response + # Rerun to display the generated image and send options + st.rerun() + + if st.session_state.image_response: + st.image(st.session_state.image_response) + # Convert PIL image to bytes for download + img_byte_arr = BytesIO() + st.session_state.image_response.save(img_byte_arr, format="JPEG") + st.download_button( + label="Download Edited Image", + data=img_byte_arr.getvalue(), + file_name="edited_image.jpeg", + mime="image/jpeg" + ) + send_image(st.session_state.image_response) + +def send_image(image_response): + st.divider() + to_email = st.text_input("Enter your email to receive the photo:", key="email_input") + if st.button("Send Email"): + with st.spinner("Sending email..."): + if to_email and image_response: + with open('app_config.json') as config_file: + config = json.load(config_file) + sender_email = config["send_email"]["sender_email"] + password = config["send_email"]["password"] + subject = "Your Spooktacular Halloween Photo! ๐ŸŽƒ๐Ÿ‘ป" + send_email_with_attachment(sender_email, password, to_email, subject, image_response) + st.success("Email sent successfully!") + else: + st.error("Please enter a valid email address and ensure the image is generated.") + + + + +def edit_image_with_ai(image, description): + with open('app_config.json') as config_file: + config = json.load(config_file) + api_key = config["nano-banana"]["api_key"] + + client = genai.Client(api_key=api_key) + + prompt = description + + response = client.models.generate_content( + model="gemini-2.5-flash-image-preview", + contents=[prompt, image], + ) + + text_response = None + image_response = None + + # AttributeError: 'NoneType' object has no attribute 'parts' + if response.candidates and len(response.candidates) <= 0: + st.error("No response from AI model. Please try again.") + return text_response, image_response + + for part in response.candidates[0].content.parts: + if part.text is not None: + text_response = part.text + + if part.inline_data is not None: + image_response = Image.open(BytesIO(part.inline_data.data)) + + return text_response, image_response diff --git a/webpages/buffteks_authenticator.py b/webpages/buffteks_authenticator.py new file mode 100644 index 000000000..491bace49 --- /dev/null +++ b/webpages/buffteks_authenticator.py @@ -0,0 +1,18 @@ + +# for authentication +import yaml +from yaml.loader import SafeLoader +import streamlit_authenticator as stauth + + +def load_authenticator(): + with open('webpages/users.yaml') as f: + users = yaml.load(f, Loader=SafeLoader) + + authenticator = stauth.Authenticate( + users['credentials'], + users['cookie']['name'], + users['cookie']['key'], + users['cookie']['expiry_days'] + ) + return authenticator \ No newline at end of file diff --git a/webpages/navigation.py b/webpages/navigation.py index cbcc2568e..d0af212d0 100644 --- a/webpages/navigation.py +++ b/webpages/navigation.py @@ -2,9 +2,13 @@ import streamlit as st import streamlit_antd_components as sac +from .buffteks_authenticator import load_authenticator + + # This doc is used to set up the navigation bar # basic structure is: def navigation_bar(): + # start page is the homepage page_label = "Homepage" @@ -44,6 +48,9 @@ def navigation_bar(): sac.MenuItem(type='divider'), sac.MenuItem("Reference", icon='paperclip'), + + sac.MenuItem(type='divider'), + sac.MenuItem("Admin", icon='lock'), ], open_all=True) diff --git a/webpages/users.yaml b/webpages/users.yaml new file mode 100644 index 000000000..b77ddf5b8 --- /dev/null +++ b/webpages/users.yaml @@ -0,0 +1,32 @@ +cookie: + expiry_days: 30 + key: buffteks.org # To be filled with any string + name: buffteks.org # To be filled with any string +credentials: + usernames: + czhang: + email: czhang@wtamu.edu + failed_login_attempts: 0 # Will be managed automatically + first_name: Carl + last_name: Zhang + logged_in: False # Will be managed automatically + password: 1992Carl@* # Will be hashed automatically + roles: # Optional + - admin + - editor + - viewer + +# oauth2: # Optional +# google: # Follow instructions: https://developers.google.com/identity/protocols/oauth2 +# client_id: # To be filled +# client_secret: # To be filled +# redirect_uri: # URL to redirect to after OAuth2 authentication +# microsoft: # Follow instructions: https://learn.microsoft.com/en-us/graph/auth-register-app-v2 +# client_id: # To be filled +# client_secret: # To be filled +# redirect_uri: # URL to redirect to after OAuth2 authentication +# tenant_id: # To be filled +# pre-authorized: # Optional +# emails: +# - melsby@gmail.com +# api_key: # Optional - register to receive a free API key: https://stauthenticator.com \ No newline at end of file