Skip to content

Commit d8ef39f

Browse files
committed
Initial commit
0 parents  commit d8ef39f

15 files changed

+2906
-0
lines changed

.replit

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
modules = ["python-3.11"]
2+
3+
[nix]
4+
channel = "stable-24_05"
5+
6+
[workflows]
7+
runButton = "Project"
8+
9+
[[workflows.workflow]]
10+
name = "Project"
11+
mode = "parallel"
12+
author = "agent"
13+
14+
[[workflows.workflow.tasks]]
15+
task = "workflow.run"
16+
args = "Start Expense Tracker"
17+
18+
[[workflows.workflow.tasks]]
19+
task = "workflow.run"
20+
args = "Expense Tracker"
21+
22+
[[workflows.workflow.tasks]]
23+
task = "workflow.run"
24+
args = "Migrate to PostgreSQL"
25+
26+
[[workflows.workflow]]
27+
name = "Start Expense Tracker"
28+
author = "agent"
29+
30+
[workflows.workflow.metadata]
31+
agentRequireRestartOnSave = false
32+
33+
[[workflows.workflow.tasks]]
34+
task = "shell.exec"
35+
args = "streamlit run main.py --server.port 5000"
36+
waitForPort = 5000
37+
38+
[[workflows.workflow]]
39+
name = "Expense Tracker"
40+
author = "agent"
41+
42+
[workflows.workflow.metadata]
43+
agentRequireRestartOnSave = false
44+
45+
[[workflows.workflow.tasks]]
46+
task = "shell.exec"
47+
args = "streamlit run main.py --server.port 5000"
48+
waitForPort = 5000
49+
50+
[[workflows.workflow]]
51+
name = "Migrate to PostgreSQL"
52+
author = "agent"
53+
54+
[workflows.workflow.metadata]
55+
agentRequireRestartOnSave = false
56+
57+
[[workflows.workflow.tasks]]
58+
task = "shell.exec"
59+
args = "python migrate_to_postgres.py"
60+
61+
[deployment]
62+
run = ["sh", "-c", "streamlit run main.py --server.port 5000"]
63+
64+
[[ports]]
65+
localPort = 5000
66+
externalPort = 80

.streamlit/config.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[server]
2+
headless = true
3+
address = "0.0.0.0"
4+
port = 5000
5+
6+
[global]
7+
dataFrameSerialization = "legacy"
8+
9+
[theme]
10+
base = "dark"
11+
primaryColor = "#F63366"
12+
backgroundColor = "#0E1117"
13+
secondaryBackgroundColor = "#262730"
14+
textColor = "#FAFAFA"
15+
font = "sans serif"

.streamlit/style.css

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/* Dark mode styles */
2+
body {
3+
color: #ffffff;
4+
background-color: #0e1117;
5+
}
6+
7+
.stButton > button {
8+
color: #ffffff;
9+
background-color: #1e2129;
10+
border-color: #4a4d56;
11+
}
12+
13+
.stTextInput > div > div > input,
14+
.stSelectbox > div > div > select {
15+
color: #ffffff;
16+
background-color: #262730;
17+
border-color: #4a4d56;
18+
}
19+
20+
.stDataFrame {
21+
color: #ffffff;
22+
}
23+
24+
/* Mobile-friendly styles */
25+
@media (max-width: 768px) {
26+
.stApp {
27+
padding: 1rem 0.5rem;
28+
}
29+
30+
.stButton > button {
31+
width: 100%;
32+
margin-bottom: 0.5rem;
33+
}
34+
35+
.stTextInput > div > div > input,
36+
.stSelectbox > div > div > select {
37+
font-size: 16px; /* Prevent zoom on mobile */
38+
}
39+
40+
.stExpander {
41+
margin-bottom: 1rem;
42+
}
43+
}
44+
45+
/* General improvements */
46+
.stApp {
47+
max-width: 1200px;
48+
margin: 0 auto;
49+
}
50+
51+
.stButton > button {
52+
border-radius: 20px;
53+
}
54+
55+
.stExpander {
56+
border: 1px solid #4a4d56;
57+
border-radius: 10px;
58+
padding: 0.5rem;
59+
}
60+
61+
/* Custom scrollbar */
62+
::-webkit-scrollbar {
63+
width: 5px;
64+
height: 5px;
65+
}
66+
67+
::-webkit-scrollbar-track {
68+
background: #262730;
69+
}
70+
71+
::-webkit-scrollbar-thumb {
72+
background: #4a4d56;
73+
border-radius: 5px;
74+
}
75+
76+
::-webkit-scrollbar-thumb:hover {
77+
background: #6e7180;
78+
}
79+
80+
/* Top menu styles */
81+
.stHorizontal {
82+
background-color: #1e2129;
83+
padding: 1rem 0;
84+
margin-bottom: 2rem;
85+
border-bottom: 1px solid #4a4d56;
86+
}
87+
88+
.stHorizontal > div > div > div > div > button {
89+
width: 100%;
90+
border-radius: 5px;
91+
background-color: #262730;
92+
color: #ffffff;
93+
border: 1px solid #4a4d56;
94+
transition: all 0.3s ease;
95+
font-weight: bold;
96+
text-transform: uppercase;
97+
font-size: 0.9rem;
98+
padding: 0.5rem 1rem;
99+
}
100+
101+
.stHorizontal > div > div > div > div > button:hover {
102+
background-color: #4a4d56;
103+
color: #ffffff;
104+
border-color: #6e7180;
105+
}
106+
107+
/* Responsive top menu */
108+
@media (max-width: 768px) {
109+
.stHorizontal {
110+
flex-direction: column;
111+
}
112+
113+
.stHorizontal > div {
114+
width: 100% !important;
115+
margin-bottom: 0.5rem;
116+
}
117+
}
118+
119+
/* Active menu item */
120+
.stHorizontal > div > div > div > div > button[data-baseweb="button"]:focus {
121+
background-color: #4a4d56;
122+
color: #ffffff;
123+
border-color: #6e7180;
124+
box-shadow: none;
125+
}

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# ExpenseTracker
2+
3+
This is the Expense Tracker project. Main object in this project is to track, compare and visualize your expenses and salary for every months.
4+
5+
User needs to register to a local DB this app has for using this app. You can use the public one from, "https://www.expensetracker.com.tr".
6+
7+
It has different tabs for each purpose. Also you can add/remove categories in your user and set your currency and language from settings page.
8+
9+
![image](https://github.com/user-attachments/assets/3533e20e-e173-4d08-9ebe-48efeb3ddb63)
10+
11+
![image](https://github.com/user-attachments/assets/5a3eccdf-eb64-4b4a-852d-ef056784f3ef)
12+
13+
![image](https://github.com/user-attachments/assets/ef64053f-efe8-42fa-bedc-ca0940c1b16f)
14+
15+
![image](https://github.com/user-attachments/assets/3def6bdf-9c47-4d77-a61f-73d1e45998e0)

auth.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import streamlit as st
2+
from sqlalchemy import create_engine, Column, Integer, String
3+
from sqlalchemy.orm import sessionmaker
4+
from sqlalchemy.ext.declarative import declarative_base
5+
from passlib.hash import bcrypt
6+
import os
7+
import jwt
8+
import datetime
9+
import logging
10+
from dotenv import load_dotenv
11+
12+
load_dotenv()
13+
14+
# Database setup
15+
engine = create_engine(os.getenv('DATABASE_URL'))
16+
Session = sessionmaker(bind=engine)
17+
Base = declarative_base()
18+
19+
# JWT configuration
20+
JWT_SECRET = os.getenv('JWT_SECRET')
21+
JWT_ALGORITHM = 'HS256'
22+
JWT_EXPIRATION_DELTA = datetime.timedelta(minutes=5)
23+
24+
class User(Base):
25+
__tablename__ = 'users'
26+
id = Column(Integer, primary_key=True)
27+
username = Column(String, unique=True, nullable=False)
28+
password_hash = Column(String, nullable=False)
29+
30+
Base.metadata.create_all(engine)
31+
32+
def hash_password(password):
33+
return bcrypt.hash(password)
34+
35+
def verify_password(plain_password, hashed_password):
36+
return bcrypt.verify(plain_password, hashed_password)
37+
38+
def register_user(username, password):
39+
username = username.lower() # Convert username to lowercase
40+
session = Session()
41+
if session.query(User).filter_by(username=username).first():
42+
return False
43+
new_user = User(username=username, password_hash=hash_password(password))
44+
session.add(new_user)
45+
session.commit()
46+
session.close()
47+
return True
48+
49+
def authenticate_user(username, password):
50+
username = username.lower() # Convert username to lowercase
51+
session = Session()
52+
user = session.query(User).filter_by(username=username).first()
53+
session.close()
54+
if user and verify_password(password, user.password_hash):
55+
return user.id
56+
return None
57+
58+
def create_token(user_id):
59+
payload = {
60+
'user_id': user_id,
61+
'exp': datetime.datetime.utcnow() + JWT_EXPIRATION_DELTA
62+
}
63+
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
64+
65+
def decode_token(token):
66+
try:
67+
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
68+
return payload['user_id']
69+
except jwt.ExpiredSignatureError:
70+
return None
71+
except jwt.InvalidTokenError:
72+
return None
73+
74+
def login():
75+
st.subheader("Login")
76+
username = st.text_input("Username", key="login_username")
77+
password = st.text_input("Password", type="password", key="login_password")
78+
if st.button("Login"):
79+
user_id = authenticate_user(username, password)
80+
if user_id:
81+
token = create_token(user_id)
82+
st.session_state.token = token
83+
st.session_state.user = username.lower() # Store lowercase username
84+
st.session_state.user_id = user_id
85+
st.success(f"Logged in as {username.lower()}")
86+
logging.info(f"Authenticated user: {username.lower()}")
87+
88+
# Store token in sessionStorage
89+
st.write("""
90+
<script>
91+
sessionStorage.setItem('jwt_token', '""" + token + """');
92+
</script>
93+
""", unsafe_allow_html=True)
94+
95+
return True
96+
else:
97+
st.error("Invalid username or password")
98+
return False
99+
100+
def logout():
101+
st.session_state.token = None
102+
st.session_state.user = None
103+
st.session_state.user_id = None
104+
105+
# Clear token from sessionStorage
106+
st.write("""
107+
<script>
108+
sessionStorage.removeItem('jwt_token');
109+
</script>
110+
""", unsafe_allow_html=True)
111+
112+
st.success("Logged out successfully")
113+
114+
def register():
115+
st.subheader("Register")
116+
username = st.text_input("Username", key="register_username")
117+
password = st.text_input("Password", type="password", key="register_password")
118+
if st.button("Register"):
119+
if register_user(username, password):
120+
st.success("Registration successful. You can now log in.")
121+
else:
122+
st.error("Username already exists. Please choose a different username.")
123+
124+
def is_authenticated():
125+
if 'token' not in st.session_state or not st.session_state.token:
126+
# Check if token exists in sessionStorage
127+
token = st.query_params.get('jwt_token')
128+
if token:
129+
user_id = decode_token(token)
130+
if user_id:
131+
st.session_state.token = token
132+
st.session_state.user_id = user_id
133+
logging.info("Token retrieved from sessionStorage")
134+
return True
135+
logging.info("No valid token found")
136+
return False
137+
138+
user_id = decode_token(st.session_state.token)
139+
if user_id:
140+
new_token = create_token(user_id)
141+
st.session_state.token = new_token
142+
143+
# Update token in sessionStorage
144+
st.write(f"""
145+
<script>
146+
sessionStorage.setItem('jwt_token', '{new_token}');
147+
</script>
148+
""", unsafe_allow_html=True)
149+
150+
logging.info("Token refreshed")
151+
return True
152+
logging.info("User not authenticated")
153+
return False
154+
155+
def authentication_required(func):
156+
def wrapper(*args, **kwargs):
157+
if is_authenticated():
158+
return func(*args, **kwargs)
159+
else:
160+
st.warning("Please log in to access this page.")
161+
login()
162+
return wrapper

0 commit comments

Comments
 (0)