diff --git a/.env.demo b/.env.demo
new file mode 100644
index 0000000..30e7571
--- /dev/null
+++ b/.env.demo
@@ -0,0 +1,74 @@
+# LeagueLedger Demo Environment Configuration
+# This file contains example values for development/demo purposes
+# DO NOT use these values in production!
+
+# Database Configuration
+DB_HOST=localhost
+DB_PORT=5432
+DB_NAME=leagueledger_demo
+DB_USER=leagueledger_user
+DB_PASSWORD=demo_password_123
+
+# Application Settings
+SECRET_KEY=demo-secret-key-replace-in-production-e8a1b2c3d4e5f6
+DEBUG=True
+ALLOWED_HOSTS=localhost,127.0.0.1,demo.leagueledger.com
+BASE_URL=http://localhost:8000
+TIMEZONE=Europe/Dublin
+
+# Email Configuration
+EMAIL_BACKEND=smtp
+EMAIL_HOST=smtp.mailgun.org
+EMAIL_PORT=587
+EMAIL_USE_TLS=True
+EMAIL_HOST_USER=postmaster@demo.leagueledger.com
+EMAIL_HOST_PASSWORD=demo_mail_password_123
+DEFAULT_FROM_EMAIL=noreply@demo.leagueledger.com
+
+# Security Settings
+# These are set to False for local development
+SESSION_COOKIE_SECURE=False
+CSRF_COOKIE_SECURE=False
+SECURE_BROWSER_XSS_FILTER=True
+SECURE_CONTENT_TYPE_NOSNIFF=True
+
+# OAuth Providers - Example values (these are not real credentials)
+# Google
+GOOGLE_CLIENT_ID=123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com
+GOOGLE_CLIENT_SECRET=GOCSPX-abcdefghijklmnopqrstuvwxyz12345
+
+# Facebook
+FACEBOOK_CLIENT_ID=1234567890123456
+FACEBOOK_CLIENT_SECRET=abcdef0123456789abcdef0123456789
+
+# GitHub
+GITHUB_CLIENT_ID=Iv1.abcdef1234567890
+GITHUB_CLIENT_SECRET=abcdef1234567890abcdef1234567890abcdef12
+
+# LinkedIn
+LINKEDIN_CLIENT_ID=abcdefghij1234567890
+LINKEDIN_CLIENT_SECRET=ABCDEfghij1234567890abcdefghijklmno
+
+# Microsoft
+MICROSOFT_CLIENT_ID=12345678-1234-1234-1234-123456789012
+MICROSOFT_CLIENT_SECRET=abc~DEF.ghiJKLmnoPQRstuvwxyz_12345678
+MICROSOFT_TENANT=common
+
+# Discord
+DISCORD_CLIENT_ID=123456789012345678
+DISCORD_CLIENT_SECRET=abcDEFghi1234567890JKLMNO_pqrSTUvwxyz
+
+# Authentik
+AUTHENTIK_CLIENT_ID=yourAuthentikClientID
+AUTHENTIK_CLIENT_SECRET=yourAuthentikClientSecret
+AUTHENTIK_CONFIG_URL=https://authentik.example.com/application/o/leagueledger/.well-known/openid-configuration
+
+# NetID
+NETID_CLIENT_ID=leagueledger-netid-client
+NETID_CLIENT_SECRET=netid-client-secret-abcdef1234567890
+
+# Feature Flags
+ENABLE_REGISTRATION=True
+ENABLE_PASSWORD_RESET=True
+ENABLE_EMAIL_VERIFICATION=True
+ENABLE_SOCIAL_LOGIN=True
\ No newline at end of file
diff --git a/.env.template b/.env.template
new file mode 100644
index 0000000..fea5103
--- /dev/null
+++ b/.env.template
@@ -0,0 +1,72 @@
+# LeagueLedger Environment Configuration Template
+# Copy this file to .env and fill in the values
+
+# Database Configuration
+DB_HOST=localhost
+DB_PORT=5432
+DB_NAME=leagueledger
+DB_USER=postgres
+DB_PASSWORD=
+
+# Application Settings
+SECRET_KEY=
+DEBUG=False
+ALLOWED_HOSTS=localhost,127.0.0.1
+BASE_URL=http://localhost:8000
+TIMEZONE=Europe/Dublin
+
+# Email Configuration
+EMAIL_BACKEND=smtp
+EMAIL_HOST=smtp.example.com
+EMAIL_PORT=587
+EMAIL_USE_TLS=True
+EMAIL_HOST_USER=
+EMAIL_HOST_PASSWORD=
+DEFAULT_FROM_EMAIL=noreply@leagueledger.com
+
+# Security Settings
+SESSION_COOKIE_SECURE=True
+CSRF_COOKIE_SECURE=True
+SECURE_BROWSER_XSS_FILTER=True
+SECURE_CONTENT_TYPE_NOSNIFF=True
+
+# OAuth Providers
+# Google
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
+
+# Facebook
+FACEBOOK_CLIENT_ID=
+FACEBOOK_CLIENT_SECRET=
+
+# GitHub
+GITHUB_CLIENT_ID=
+GITHUB_CLIENT_SECRET=
+
+# LinkedIn
+LINKEDIN_CLIENT_ID=
+LINKEDIN_CLIENT_SECRET=
+
+# Microsoft
+MICROSOFT_CLIENT_ID=
+MICROSOFT_CLIENT_SECRET=
+MICROSOFT_TENANT=common
+
+# Discord
+DISCORD_CLIENT_ID=
+DISCORD_CLIENT_SECRET=
+
+# Authentik
+AUTHENTIK_CLIENT_ID=
+AUTHENTIK_CLIENT_SECRET=
+AUTHENTIK_CONFIG_URL=
+
+# NetID
+NETID_CLIENT_ID=
+NETID_CLIENT_SECRET=
+
+# Feature Flags
+ENABLE_REGISTRATION=True
+ENABLE_PASSWORD_RESET=True
+ENABLE_EMAIL_VERIFICATION=True
+ENABLE_SOCIAL_LOGIN=True
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..67a3eb3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,58 @@
+# LeagueLedger
+
+A modern web application for managing sports league loyalty programs, events, and rewards through QR codes.
+
+## Features
+
+- **Team Management**: Create and manage teams with member profiles
+- **QR Code Redemption**: Generate and scan QR codes for points and rewards
+- **Leaderboards**: Track team and individual standings
+- **OAuth Integration**: Multiple social login options including Google, GitHub, LinkedIn, Facebook, and netID
+- **Internationalization**: Multi-language support with English and German locales
+- **Responsive Design**: Mobile-friendly interface built with Tailwind CSS
+
+## Quick Start
+
+1. **Clone the repository**
+ ```
+ git clone https://github.com/yourusername/LeagueLedger.git
+ cd LeagueLedger
+ ```
+
+2. **Set up environment variables**
+ ```
+ cp .env.template .env
+ # Edit .env with your configuration
+ ```
+
+3. **Run with Docker**
+ ```
+ docker-compose up -d
+ ```
+
+4. **Or run locally**
+ ```
+ pip install -r requirements.txt
+ python app/main.py
+ ```
+
+5. **Access the application**
+ ```
+ http://localhost:8000
+ ```
+
+## Documentation
+
+Detailed documentation is available in the `/docs` directory. Generate the complete documentation site with:
+
+```
+mkdocs serve
+```
+
+## License
+
+This project is licensed under the terms of the license file included in this repository.
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
\ No newline at end of file
diff --git a/app/auth/oauth.py b/app/auth/oauth.py
index 5b6f3fa..84ed1ac 100644
--- a/app/auth/oauth.py
+++ b/app/auth/oauth.py
@@ -818,6 +818,113 @@ class LinkedInOAuth(OAuthProvider):
"raw": user_info
}
+class NetIDOAuth(OAuthProvider):
+ """NetID OAuth provider implementation"""
+
+ provider_id = "netid"
+ display_name = "netID"
+ icon_class = "fas fa-check" # Could be replaced with a custom netID icon class if available
+ button_color = "#76b82a"
+
+ # NetID OIDC endpoints
+ AUTHORIZATION_URL = "https://broker.netid.de/authorize"
+ TOKEN_URL = "https://broker.netid.de/token"
+ USERINFO_URL = "https://broker.netid.de/userinfo"
+
+ def __init__(self):
+ self.client_id = os.getenv("NETID_CLIENT_ID", "")
+ self.client_secret = os.getenv("NETID_CLIENT_SECRET", "")
+ super().__init__()
+
+ def initialize_client(self):
+ if not self.client_id or not self.client_secret:
+ self.client = None
+ return
+
+ try:
+ # Create a custom OAuth2 client for NetID OpenID Connect
+ from httpx_oauth.oauth2 import OAuth2
+ self.client = OAuth2(
+ client_id=self.client_id,
+ client_secret=self.client_secret,
+ authorize_endpoint=self.AUTHORIZATION_URL,
+ access_token_endpoint=self.TOKEN_URL,
+ refresh_token_endpoint=self.TOKEN_URL,
+ base_scopes=["openid", "email", "profile"]
+ )
+ except Exception as e:
+ print(f"Error initializing NetID OAuth client: {str(e)}")
+ self.client = None
+
+ async def get_login_url(self, request: Request, redirect_uri: str) -> str:
+ if not self.client:
+ self.initialize_client()
+
+ if not self.client:
+ raise HTTPException(status_code=500, detail="NetID OAuth client could not be initialized")
+
+ try:
+ authorization_url = await self.client.get_authorization_url(
+ redirect_uri=redirect_uri,
+ scope=["openid", "email", "profile"],
+ state=str(request.session.get("session_id", ""))
+ )
+ return authorization_url
+ except Exception as e:
+ print(f"Error getting NetID authorization URL: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"OAuth error: {str(e)}")
+
+ async def get_user_info(self, request: Request, redirect_uri: str, code: str) -> Dict[str, Any]:
+ if not self.client:
+ self.initialize_client()
+
+ if not self.client:
+ raise HTTPException(status_code=500, detail="NetID OAuth client could not be initialized")
+
+ try:
+ # Exchange code for token
+ token = await self.client.get_access_token(
+ code=code,
+ redirect_uri=redirect_uri
+ )
+
+ access_token = token.get("access_token")
+
+ if not access_token:
+ raise HTTPException(status_code=400, detail="Could not get NetID access token")
+
+ # Get user info from NetID UserInfo endpoint
+ async with httpx.AsyncClient() as client:
+ headers = {"Authorization": f"Bearer {access_token}"}
+ response = await client.get(
+ self.USERINFO_URL,
+ headers=headers
+ )
+
+ if response.status_code != 200:
+ raise HTTPException(status_code=500, detail=f"Error fetching NetID user info: {response.text}")
+
+ return response.json()
+
+ except GetAccessTokenError as e:
+ error_description = e.args[0]
+ raise HTTPException(status_code=400, detail=f"NetID OAuth error: {error_description}")
+ except Exception as e:
+ print(f"Error getting NetID user info: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"OAuth error: {str(e)}")
+
+ def get_normalized_user_data(self, user_info: Dict[str, Any]) -> Dict[str, Any]:
+ # NetID OpenID Connect response normalization
+ return {
+ "id": user_info.get("sub", ""), # 'sub' is the standard OIDC subject identifier
+ "email": user_info.get("email"),
+ "name": f"{user_info.get('given_name', '')} {user_info.get('family_name', '')}".strip(),
+ "first_name": user_info.get("given_name"),
+ "last_name": user_info.get("family_name"),
+ "picture": None, # NetID might not provide profile picture
+ "raw": user_info
+ }
+
class OAuthManager:
"""
Manager class for handling multiple OAuth providers
@@ -836,6 +943,7 @@ class OAuthManager:
self.register_provider(MicrosoftOAuth())
self.register_provider(DiscordOAuth())
self.register_provider(LinkedInOAuth())
+ self.register_provider(NetIDOAuth()) # Register NetID provider
def register_provider(self, provider: OAuthProvider):
"""Register a new provider"""
diff --git a/app/static/css/styles.css b/app/static/css/styles.css
index f6e753c..613799f 100644
--- a/app/static/css/styles.css
+++ b/app/static/css/styles.css
@@ -1,7 +1,7 @@
/*
* LeagueLedger Main Stylesheet
* Version: 1.0.0
- * Date: April 13, 2025
+ * Date: April 15, 2025
*/
:root {
@@ -155,4 +155,325 @@ h1, h2, h3, h4, h5, h6 {
font-weight: bold;
color: var(--irish-green);
}
+}
+
+/* Google Sign-In Button Styles */
+.gsi-material-button {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ -webkit-appearance: none;
+ background-color: WHITE;
+ background-image: none;
+ border: 1px solid #747775;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ color: #1f1f1f;
+ cursor: pointer;
+ font-family: 'Roboto', arial, sans-serif;
+ font-size: 14px;
+ height: 40px;
+ letter-spacing: 0.25px;
+ outline: none;
+ overflow: hidden;
+ padding: 0 12px;
+ position: relative;
+ text-align: center;
+ -webkit-transition: background-color .218s, border-color .218s, box-shadow .218s;
+ transition: background-color .218s, border-color .218s, box-shadow .218s;
+ vertical-align: middle;
+ white-space: nowrap;
+ width: auto;
+ max-width: 400px;
+ min-width: min-content;
+ }
+
+.gsi-material-button .gsi-material-button-icon {
+ height: 20px;
+ margin-right: 12px;
+ min-width: 20px;
+ width: 20px;
+}
+
+.gsi-material-button .gsi-material-button-content-wrapper {
+ -webkit-align-items: center;
+ align-items: center;
+ display: flex;
+ -webkit-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ height: 100%;
+ justify-content: space-between;
+ position: relative;
+ width: 100%;
+}
+
+.gsi-material-button .gsi-material-button-contents {
+ -webkit-flex-grow: 1;
+ flex-grow: 1;
+ font-family: 'Roboto', arial, sans-serif;
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: top;
+}
+
+.gsi-material-button .gsi-material-button-state {
+ -webkit-transition: opacity .218s;
+ transition: opacity .218s;
+ bottom: 0;
+ left: 0;
+ opacity: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.gsi-material-button:disabled {
+ cursor: default;
+ background-color: #ffffff61;
+ border-color: #1f1f1f1f;
+}
+
+.gsi-material-button:disabled .gsi-material-button-contents {
+ opacity: 38%;
+}
+
+.gsi-material-button:disabled .gsi-material-button-icon {
+ opacity: 38%;
+}
+
+.gsi-material-button:not(:disabled):active .gsi-material-button-state,
+.gsi-material-button:not(:disabled):focus .gsi-material-button-state {
+ background-color: #303030;
+ opacity: 12%;
+}
+
+.gsi-material-button:not(:disabled):hover {
+ -webkit-box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .30), 0 1px 3px 1px rgba(60, 64, 67, .15);
+ box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .30), 0 1px 3px 1px rgba(60, 64, 67, .15);
+}
+
+.gsi-material-button:not(:disabled):hover .gsi-material-button-state {
+ background-color: #303030;
+ opacity: 8%;
+}
+
+/* OAuth Buttons Container */
+.oauth-buttons-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ max-width: 280px;
+ margin: 0 auto;
+}
+
+/* LinkedIn Login Button */
+.linkedin-login-button {
+ background-color: #0077B5;
+ border: none;
+ border-radius: 4px;
+ color: white;
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ height: 40px;
+ letter-spacing: normal;
+ padding: 0;
+ position: relative;
+ text-align: center;
+ transition: background-color .218s;
+ width: 100%;
+ max-width: 280px;
+ margin: 0 auto 8px;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.linkedin-login-button:hover {
+ background-color: #006097;
+}
+
+.linkedin-login-button .linkedin-icon {
+ display: inline-block;
+ margin-right: 8px;
+ height: 18px;
+ width: 18px;
+}
+
+.linkedin-login-button .linkedin-icon svg {
+ height: 18px;
+ width: 18px;
+ fill: white;
+}
+
+.linkedin-login-button .button-text {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+/* Facebook Login Button */
+.fb-login-button {
+ background-color: #1877F2;
+ border: none;
+ border-radius: 4px;
+ color: white;
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ height: 40px;
+ letter-spacing: normal;
+ padding: 0;
+ position: relative;
+ text-align: center;
+ transition: background-color .218s;
+ width: 100%;
+ max-width: 280px;
+ margin: 0 auto 8px;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.fb-login-button:hover {
+ background-color: #166FE5;
+}
+
+.fb-login-button .fb-icon {
+ display: inline-block;
+ margin-right: 8px;
+ height: 18px;
+ width: 18px;
+ background-color: white;
+ border-radius: 2px;
+}
+
+.fb-login-button .fb-icon svg {
+ height: 18px;
+ width: 18px;
+ fill: #1877F2;
+}
+
+.fb-login-button .button-text {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+/* GitHub Login Button */
+.github-login-button {
+ background-color: #24292e;
+ border: 1px solid rgba(27, 31, 35, 0.15);
+ border-radius: 6px;
+ color: white;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ height: 40px;
+ padding: 0;
+ position: relative;
+ text-align: center;
+ transition: background-color 0.2s;
+ width: 100%;
+ max-width: 280px;
+ margin: 0 auto 8px;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.github-login-button:hover {
+ background-color: #2c3136;
+}
+
+.github-login-button .github-icon {
+ display: inline-block;
+ margin-right: 8px;
+ height: 20px;
+ width: 20px;
+}
+
+.github-login-button .github-icon svg {
+ height: 20px;
+ width: 20px;
+ fill: white;
+}
+
+.github-login-button .button-text {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+/* NetID Login Button */
+.netid-login-button {
+ background-color: #76b82a;
+ border: none;
+ border-radius: 3px;
+ color: white;
+ font-family: 'IBM Plex Sans', Verdana, Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ height: 40px;
+ padding: 0;
+ position: relative;
+ text-align: center;
+ transition: background-color 0.2s;
+ width: 100%;
+ max-width: 280px;
+ margin: 0 auto 8px;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.netid-login-button:hover {
+ background-color: #5d9422;
+}
+
+.netid-login-button .netid-icon {
+ display: inline-block;
+ margin-right: 8px;
+ height: 20px;
+ width: 20px;
+}
+
+.netid-login-button .netid-icon svg {
+ height: 20px;
+ width: 20px;
+ fill: white;
+}
+
+.netid-login-button .button-text {
+ font-family: 'IBM Plex Sans', Verdana, Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+/* Other OAuth provider button customizations */
+.oauth-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+ margin-bottom: 8px;
+ text-decoration: none;
}
\ No newline at end of file
diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html
index 131d0fe..d3973c2 100644
--- a/app/templates/auth/login.html
+++ b/app/templates/auth/login.html
@@ -91,30 +91,91 @@
Or sign in with
- - {% if oauth_providers|length <= 2 %} -