Skip to content

Commit 2766fbe

Browse files
committed
commit 9f1b2c3d4e5f67890123456789abcdef01234567
1 parent 58a81f9 commit 2766fbe

File tree

378 files changed

+56791
-9
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

378 files changed

+56791
-9
lines changed

Dockerfile

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
### Multi-stage Dockerfile for Flask app (production)
1+
22
FROM python:3.11-slim as builder
33

44
ENV PYTHONDONTWRITEBYTECODE=1
55
ENV PYTHONUNBUFFERED=1
66

77
WORKDIR /app
88

9-
# Install build deps
109
RUN apt-get update && apt-get install -y --no-install-recommends build-essential gcc curl && rm -rf /var/lib/apt/lists/*
1110

12-
# Copy only requirements first for better caching
11+
1312
COPY requirements.txt /app/requirements.txt
1413
RUN python -m pip install --upgrade pip
1514
RUN pip install --no-cache-dir -r requirements.txt
@@ -20,13 +19,12 @@ WORKDIR /app
2019
ENV PYTHONDONTWRITEBYTECODE=1
2120
ENV PYTHONUNBUFFERED=1
2221

23-
# Copy installed packages from builder
22+
2423
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
2524

26-
# Copy app sources
2725
COPY . /app
2826

2927
EXPOSE 5000
3028

31-
# Use gunicorn in production
29+
3230
CMD ["gunicorn", "run:app", "-w", "4", "-b", "0.0.0.0:5000", "--chdir", "/app"]

app.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
from flask import Flask
2-
from routes import main
2+
from authlib.integrations.flask_client import OAuth
3+
from routes import main
4+
35

46
def create_app():
57
app = Flask(__name__)
6-
8+
79
# Load configuration from config.py
810
app.config.from_pyfile('config.py')
911

12+
# Initialize OAuth
13+
oauth = OAuth(app)
14+
# Register common providers (config values must be provided in config.py or env)
15+
oauth.register('github', client_id=app.config.get('GITHUB_CLIENT_ID'), client_secret=app.config.get('GITHUB_CLIENT_SECRET'), access_token_url='https://github.com/login/oauth/access_token', authorize_url='https://github.com/login/oauth/authorize', api_base_url='https://api.github.com/', client_kwargs={'scope':'user:email'})
16+
oauth.register('gitlab', client_id=app.config.get('GITLAB_CLIENT_ID'), client_secret=app.config.get('GITLAB_CLIENT_SECRET'), access_token_url='https://gitlab.com/oauth/token', authorize_url='https://gitlab.com/oauth/authorize', api_base_url='https://gitlab.com/api/v4')
17+
oauth.register('linkedin', client_id=app.config.get('LINKEDIN_CLIENT_ID'), client_secret=app.config.get('LINKEDIN_CLIENT_SECRET'), access_token_url='https://www.linkedin.com/oauth/v2/accessToken', authorize_url='https://www.linkedin.com/oauth/v2/authorization', api_base_url='https://api.linkedin.com/v2', client_kwargs={'scope':'r_liteprofile r_emailaddress'})
18+
oauth.register('facebook', client_id=app.config.get('FACEBOOK_CLIENT_ID'), client_secret=app.config.get('FACEBOOK_CLIENT_SECRET'), access_token_url='https://graph.facebook.com/v10.0/oauth/access_token', authorize_url='https://www.facebook.com/v10.0/dialog/oauth', api_base_url='https://graph.facebook.com/')
19+
20+
# Attach oauth instance to app for routes to use
21+
app.oauth = oauth
22+
1023
# Register your blueprint
1124
app.register_blueprint(main)
1225

config.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
11
SECRET_KEY = 'your-secure-key'
2-
UPLOAD_FOLDER = '/path/to/uploads'
2+
UPLOAD_FOLDER = '/path/to/uploads'
3+
# OAuth client credentials (set these via environment variables or directly for local testing)
4+
# Example: GITHUB_CLIENT_ID = 'xxx'
5+
GITHUB_CLIENT_ID = None
6+
GITHUB_CLIENT_SECRET = None
7+
GITLAB_CLIENT_ID = None
8+
GITLAB_CLIENT_SECRET = None
9+
LINKEDIN_CLIENT_ID = None
10+
LINKEDIN_CLIENT_SECRET = None
11+
FACEBOOK_CLIENT_ID = None
12+
FACEBOOK_CLIENT_SECRET = None

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Flask-Babel
1919
Flask-Cors
2020
python-dotenv
2121
openai
22+
authlib
2223
alembic
2324
marshmallow
2425
python-magic

routes.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ def inject_globals():
2020

2121
@main.route('/')
2222
def dashboard():
23+
# require login to view dashboard
24+
user_id = session.get('user_id')
25+
if not user_id:
26+
flash('Please log in to access the dashboard.', 'error')
27+
return redirect(url_for('main.login', next=url_for('main.dashboard')))
2328
return render_template('dashboard.html')
2429

2530
@main.route('/upload', methods=['POST'])
@@ -74,13 +79,101 @@ def login():
7479
# If profile incomplete, redirect to complete profile
7580
if not user.get('full_name') or not user.get('company'):
7681
return redirect(url_for('main.complete_profile'))
82+
# Respect optional `next` param so we can redirect to dashboard or original page
83+
next_url = request.args.get('next') or request.form.get('next')
84+
if next_url:
85+
return redirect(next_url)
7786
return redirect(url_for('main.dashboard'))
7887
else:
7988
flash('Invalid credentials.', 'error')
8089
return render_template('login.html')
8190
return render_template('login.html')
8291

8392

93+
@main.route('/login/<provider>')
94+
def oauth_login(provider):
95+
# start oauth login flow
96+
oauth = current_app.oauth
97+
if provider not in oauth._registry:
98+
flash('Unknown OAuth provider.', 'error')
99+
return redirect(url_for('main.login'))
100+
redirect_uri = url_for('main.oauth_callback', provider=provider, _external=True)
101+
return oauth.create_client(provider).authorize_redirect(redirect_uri)
102+
103+
104+
@main.route('/auth/<provider>/callback')
105+
def oauth_callback(provider):
106+
oauth = current_app.oauth
107+
client = oauth.create_client(provider)
108+
if not client:
109+
flash('OAuth client not configured.', 'error')
110+
return redirect(url_for('main.login'))
111+
token = client.authorize_access_token()
112+
if not token:
113+
flash('Authentication failed.', 'error')
114+
return redirect(url_for('main.login'))
115+
# fetch basic profile information depending on provider
116+
profile = {}
117+
if provider == 'github':
118+
profile = client.get('user').json()
119+
email = profile.get('email') or (client.get('user/emails').json()[0].get('email') if client.get('user/emails') else None)
120+
elif provider == 'gitlab':
121+
profile = client.get('user').json()
122+
email = profile.get('email')
123+
elif provider == 'linkedin':
124+
# LinkedIn requires separate endpoints for email and profile
125+
profile = client.get('me').json()
126+
email_resp = client.get('emailAddress?q=members&projection=(elements*(handle~))')
127+
email = None
128+
try:
129+
email = email_resp.json().get('elements', [])[0].get('handle~', {}).get('emailAddress')
130+
except Exception:
131+
email = None
132+
elif provider == 'facebook':
133+
profile = client.get('me?fields=id,name,email').json()
134+
email = profile.get('email')
135+
else:
136+
email = None
137+
138+
if not email:
139+
flash('Could not retrieve email from provider. Please sign up with email/password.', 'error')
140+
return redirect(url_for('main.signup'))
141+
142+
# If user exists, log them in; otherwise create an account (random password)
143+
user = authenticate_user(email, '')
144+
# authenticate_user expects a password; instead we'll check existence directly
145+
from dbkamp.db import get_connection
146+
conn = get_connection()
147+
cur = conn.cursor()
148+
cur.execute('SELECT * FROM users WHERE email = ?', (email,))
149+
row = cur.fetchone()
150+
conn.close()
151+
if row:
152+
session['user_id'] = row['id']
153+
session['user_email'] = row['email']
154+
flash('Logged in with ' + provider.capitalize(), 'success')
155+
return redirect(url_for('main.dashboard'))
156+
else:
157+
# create user with a random password placeholder
158+
import secrets
159+
pw = secrets.token_urlsafe(16)
160+
created = create_user(email, pw)
161+
if created:
162+
# fetch new user id
163+
conn = get_connection()
164+
cur = conn.cursor()
165+
cur.execute('SELECT * FROM users WHERE email = ?', (email,))
166+
new = cur.fetchone()
167+
conn.close()
168+
session['user_id'] = new['id']
169+
session['user_email'] = new['email']
170+
flash('Account created via ' + provider.capitalize(), 'success')
171+
return redirect(url_for('main.complete_profile'))
172+
else:
173+
flash('Unable to create account.', 'error')
174+
return redirect(url_for('main.signup'))
175+
176+
84177
@main.route('/resources')
85178
def resources():
86179
return render_template('resources.html')

templates/login.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ <h1>Welcome back</h1>
1414
<button class="btn-signup" type="submit">Log in</button>
1515
</div>
1616
</form>
17+
<div style="margin-top:1rem">
18+
<p class="muted">Or continue with</p>
19+
<div style="display:flex;gap:0.5rem;margin-top:0.5rem">
20+
<a class="btn" href="/login/github">GitHub</a>
21+
<a class="btn" href="/login/gitlab">GitLab</a>
22+
<a class="btn" href="/login/linkedin">LinkedIn</a>
23+
<a class="btn" href="/login/facebook">Facebook</a>
24+
</div>
25+
</div>
1726
</div>
1827
</div>
1928
{% endblock %}

templates/signup.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ <h1>Create your account</h1>
1414
<button class="btn-signup" type="submit">Create Account</button>
1515
</div>
1616
</form>
17+
<div style="margin-top:1rem">
18+
<p class="muted">Or sign up with</p>
19+
<div style="display:flex;gap:0.5rem;margin-top:0.5rem">
20+
<a class="btn" href="/login/github">GitHub</a>
21+
<a class="btn" href="/login/gitlab">GitLab</a>
22+
<a class="btn" href="/login/linkedin">LinkedIn</a>
23+
<a class="btn" href="/login/facebook">Facebook</a>
24+
</div>
25+
</div>
1726
</div>
1827
</div>
1928
{% endblock %}
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pip
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
Metadata-Version: 2.4
2+
Name: Authlib
3+
Version: 1.6.3
4+
Summary: The ultimate Python library in building OAuth and OpenID Connect servers and clients.
5+
Author-email: Hsiaoming Yang <me@lepture.com>
6+
License: BSD-3-Clause
7+
Project-URL: Documentation, https://docs.authlib.org/
8+
Project-URL: Purchase, https://authlib.org/plans
9+
Project-URL: Issues, https://github.com/authlib/authlib/issues
10+
Project-URL: Source, https://github.com/authlib/authlib
11+
Project-URL: Donate, https://github.com/sponsors/lepture
12+
Project-URL: Blog, https://blog.authlib.org/
13+
Classifier: Development Status :: 5 - Production/Stable
14+
Classifier: Environment :: Console
15+
Classifier: Environment :: Web Environment
16+
Classifier: Intended Audience :: Developers
17+
Classifier: License :: OSI Approved :: BSD License
18+
Classifier: Operating System :: OS Independent
19+
Classifier: Programming Language :: Python
20+
Classifier: Programming Language :: Python :: 3
21+
Classifier: Programming Language :: Python :: 3.9
22+
Classifier: Programming Language :: Python :: 3.10
23+
Classifier: Programming Language :: Python :: 3.11
24+
Classifier: Programming Language :: Python :: 3.12
25+
Classifier: Programming Language :: Python :: 3.13
26+
Classifier: Programming Language :: Python :: Implementation :: CPython
27+
Classifier: Programming Language :: Python :: Implementation :: PyPy
28+
Classifier: Topic :: Security
29+
Classifier: Topic :: Security :: Cryptography
30+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
31+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
32+
Requires-Python: >=3.9
33+
Description-Content-Type: text/x-rst
34+
License-File: LICENSE
35+
Requires-Dist: cryptography
36+
Dynamic: license-file

0 commit comments

Comments
 (0)