Commit f4c1b651 authored by Your Name's avatar Your Name

User signup and oauth2 signup/login support

parent 51a7e2a5
...@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
-**OAuth2 Authentication Support**
- Google OAuth2 authentication and signup
- GitHub OAuth2 authentication and signup
- Admin configurable OAuth2 providers in dashboard settings
- Dynamic OAuth2 button display on login and signup pages
- Automatic user account creation for first time OAuth users
- Automatic email verification for OAuth authenticated users
- Full session integration matching existing authentication system
- PKCE security implemented for Google OAuth2 flow
- State parameter validation for CSRF protection
- Proxy-aware redirect URI handling
- Secure random password generation for OAuth users
## [0.99.20] - 2026-04-11 ## [0.99.20] - 2026-04-11
### Added ### Added
......
...@@ -25,6 +25,8 @@ from .kiro import KiroAuthManager, AuthType ...@@ -25,6 +25,8 @@ from .kiro import KiroAuthManager, AuthType
from .claude import ClaudeAuth from .claude import ClaudeAuth
from .kilo import KiloOAuth2 from .kilo import KiloOAuth2
from .qwen import QwenOAuth2 from .qwen import QwenOAuth2
from .google import GoogleOAuth2
from .github import GitHubOAuth2
__all__ = [ __all__ = [
"KiroAuthManager", "KiroAuthManager",
...@@ -32,4 +34,6 @@ __all__ = [ ...@@ -32,4 +34,6 @@ __all__ = [
"ClaudeAuth", "ClaudeAuth",
"KiloOAuth2", "KiloOAuth2",
"QwenOAuth2", "QwenOAuth2",
"GoogleOAuth2",
"GitHubOAuth2",
] ]
# Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import json
import logging
import urllib.parse
import secrets
from typing import Dict, Optional
import httpx
logger = logging.getLogger(__name__)
GITHUB_OAUTH_AUTH_URL = "https://github.com/login/oauth/authorize"
GITHUB_OAUTH_TOKEN_URL = "https://github.com/login/oauth/access_token"
GITHUB_API_USER_URL = "https://api.github.com/user"
GITHUB_API_EMAILS_URL = "https://api.github.com/user/emails"
class GitHubOAuth2:
"""GitHub OAuth2 Authentication Handler"""
def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
self._state = None
def get_authorization_url(self, scopes: list = None) -> str:
"""Generate GitHub authorization URL"""
if scopes is None:
scopes = ["user:email", "read:user"]
state = secrets.token_urlsafe(16)
self._state = state
params = {
"client_id": self.client_id,
"redirect_uri": self.redirect_uri,
"scope": " ".join(scopes),
"state": state,
"allow_signup": "true"
}
query = urllib.parse.urlencode(params)
return f"{GITHUB_OAUTH_AUTH_URL}?{query}"
async def exchange_code_for_tokens(self, code: str, state: str) -> Optional[Dict]:
"""Exchange authorization code for access token"""
if state != self._state:
logger.warning("GitHub OAuth2: State parameter mismatch")
return None
try:
async with httpx.AsyncClient() as client:
response = await client.post(
GITHUB_OAUTH_TOKEN_URL,
data={
"code": code,
"client_id": self.client_id,
"client_secret": self.client_secret,
"redirect_uri": self.redirect_uri
},
headers={"Accept": "application/json"}
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"GitHub OAuth2 token exchange failed: {e}")
return None
async def get_user_info(self, access_token: str) -> Optional[Dict]:
"""Get user information from GitHub including primary email"""
try:
async with httpx.AsyncClient() as client:
# Get user profile
user_response = await client.get(
GITHUB_API_USER_URL,
headers={"Authorization": f"Bearer {access_token}"}
)
user_response.raise_for_status()
user_data = user_response.json()
# Get emails if needed
if not user_data.get("email"):
emails_response = await client.get(
GITHUB_API_EMAILS_URL,
headers={"Authorization": f"Bearer {access_token}"}
)
emails_response.raise_for_status()
emails = emails_response.json()
# Find primary verified email
for email in emails:
if email.get("primary") and email.get("verified"):
user_data["email"] = email.get("email")
break
return user_data
except Exception as e:
logger.error(f"GitHub OAuth2 user info request failed: {e}")
return None
# Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import json
import logging
import urllib.parse
import secrets
import hashlib
import base64
from typing import Dict, Optional, Tuple
import httpx
logger = logging.getLogger(__name__)
GOOGLE_OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token"
GOOGLE_OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"
class GoogleOAuth2:
"""Google OAuth2 Authentication Handler"""
def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
self._state = None
self._code_verifier = None
def generate_pkce_pair(self) -> Tuple[str, str]:
"""Generate PKCE code verifier and challenge"""
code_verifier = secrets.token_urlsafe(32)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).decode().rstrip('=')
return code_verifier, code_challenge
def get_authorization_url(self, scopes: list = None) -> str:
"""Generate authorization URL with PKCE"""
if scopes is None:
scopes = ["openid", "email", "profile"]
state = secrets.token_urlsafe(16)
code_verifier, code_challenge = self.generate_pkce_pair()
self._state = state
self._code_verifier = code_verifier
params = {
"client_id": self.client_id,
"redirect_uri": self.redirect_uri,
"response_type": "code",
"scope": " ".join(scopes),
"state": state,
"code_challenge": code_challenge,
"code_challenge_method": "S256",
"access_type": "offline",
"prompt": "select_account consent"
}
query = urllib.parse.urlencode(params)
return f"{GOOGLE_OAUTH_AUTH_URL}?{query}"
async def exchange_code_for_tokens(self, code: str, state: str) -> Optional[Dict]:
"""Exchange authorization code for tokens"""
if state != self._state:
logger.warning("Google OAuth2: State parameter mismatch")
return None
try:
async with httpx.AsyncClient() as client:
response = await client.post(
GOOGLE_OAUTH_TOKEN_URL,
data={
"code": code,
"client_id": self.client_id,
"client_secret": self.client_secret,
"redirect_uri": self.redirect_uri,
"grant_type": "authorization_code",
"code_verifier": self._code_verifier
},
headers={"Accept": "application/json"}
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Google OAuth2 token exchange failed: {e}")
return None
async def get_user_info(self, access_token: str) -> Optional[Dict]:
"""Get user information from Google"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
GOOGLE_OAUTH_USERINFO_URL,
headers={"Authorization": f"Bearer {access_token}"}
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Google OAuth2 user info request failed: {e}")
return None
...@@ -131,5 +131,20 @@ ...@@ -131,5 +131,20 @@
"use_ssl": false, "use_ssl": false,
"from_email": "noreply@example.com", "from_email": "noreply@example.com",
"from_name": "AISBF" "from_name": "AISBF"
},
"oauth2": {
"google": {
"enabled": false,
"client_id": "",
"client_secret": "",
"scopes": ["openid", "email", "profile"]
},
"github": {
"enabled": false,
"client_id": "",
"client_secret": "",
"scopes": ["user:email", "read:user"]
}
} }
} }
This diff is collapsed.
...@@ -144,6 +144,8 @@ setup( ...@@ -144,6 +144,8 @@ setup(
'aisbf/auth/kilo.py', 'aisbf/auth/kilo.py',
'aisbf/auth/codex.py', 'aisbf/auth/codex.py',
'aisbf/auth/qwen.py', 'aisbf/auth/qwen.py',
'aisbf/auth/google.py',
'aisbf/auth/github.py',
]), ]),
# Install dashboard templates # Install dashboard templates
('share/aisbf/templates', [ ('share/aisbf/templates', [
...@@ -168,6 +170,8 @@ setup( ...@@ -168,6 +170,8 @@ setup(
'templates/dashboard/rate_limits.html', 'templates/dashboard/rate_limits.html',
'templates/dashboard/users.html', 'templates/dashboard/users.html',
'templates/dashboard/signup.html', 'templates/dashboard/signup.html',
'templates/dashboard/profile.html',
'templates/dashboard/change_password.html',
]), ]),
# Install static files (extension and favicon) # Install static files (extension and favicon)
('share/aisbf/static', [ ('share/aisbf/static', [
......
...@@ -53,6 +53,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -53,6 +53,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #0f3460; color: #e0e0e0; } th, td { padding: 12px; text-align: left; border-bottom: 1px solid #0f3460; color: #e0e0e0; }
th { background: #0f3460; font-weight: 600; color: #e0e0e0; } th { background: #0f3460; font-weight: 600; color: #e0e0e0; }
.code { background: #0f3460; padding: 15px; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 13px; overflow-x: auto; color: #e0e0e0; } .code { background: #0f3460; padding: 15px; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 13px; overflow-x: auto; color: #e0e0e0; }
.nav .account-menu { position: relative; margin-left: auto; }
.nav .account-trigger { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 4px; cursor: pointer; color: #a0a0a0; }
.nav .account-trigger:hover { background: #0f3460; color: #e0e0e0; }
.nav .account-trigger img { width: 24px; height: 24px; border-radius: 50%; vertical-align: middle; }
.nav .account-dropdown { position: absolute; top: 100%; right: 0; background: #16213e; border-radius: 8px; min-width: 200px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); margin-top: 8px; display: none; z-index: 100; border: 1px solid #0f3460; }
.nav .account-dropdown.active { display: block; }
.nav .account-dropdown a { display: block; padding: 12px 16px; color: #a0a0a0; text-decoration: none; border-radius: 0; }
.nav .account-dropdown a:first-child { border-radius: 8px 8px 0 0; }
.nav .account-dropdown a:last-child { border-radius: 0 0 8px 8px; }
.nav .account-dropdown a:hover { background: #0f3460; color: #e0e0e0; }
</style> </style>
<script> <script>
/* /*
...@@ -71,6 +81,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -71,6 +81,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
function toggleAccountMenu() {
const dropdown = document.getElementById('account-dropdown');
dropdown.classList.toggle('active');
}
// Close dropdown when clicking outside
document.addEventListener('click', function(event) {
const accountMenu = document.querySelector('.account-menu');
const dropdown = document.getElementById('account-dropdown');
if (!accountMenu.contains(event.target)) {
dropdown.classList.remove('active');
}
});
function restartServer() { function restartServer() {
if (confirm('Are you sure you want to restart the server? This will disconnect all active connections.')) { if (confirm('Are you sure you want to restart the server? This will disconnect all active connections.')) {
fetch('{{ url_for(request, "/dashboard/restart") }}', { fetch('{{ url_for(request, "/dashboard/restart") }}', {
...@@ -424,6 +448,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -424,6 +448,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% endif %} {% endif %}
<a href="{{ url_for(request, '/dashboard/docs') }}" {% if '/docs' in request.path %}class="active"{% endif %}>Docs</a> <a href="{{ url_for(request, '/dashboard/docs') }}" {% if '/docs' in request.path %}class="active"{% endif %}>Docs</a>
<a href="{{ url_for(request, '/dashboard/about') }}" {% if '/about' in request.path %}class="active"{% endif %}>About</a> <a href="{{ url_for(request, '/dashboard/about') }}" {% if '/about' in request.path %}class="active"{% endif %}>About</a>
{% if request.session.user_id %}
<div class="account-menu">
<div class="account-trigger" onclick="toggleAccountMenu()">
<img src="https://www.gravatar.com/avatar/{{ request.session.email|md5 if request.session.email else '' }}?s=48&d=identicon" alt="User avatar">
<span>Account</span>
</div>
<div class="account-dropdown" id="account-dropdown">
{% if request.session.user_id %}
{% if request.session.user_id %}
<a href="{{ url_for(request, '/dashboard/profile') }}">Edit Profile</a>
<a href="{{ url_for(request, '/dashboard/change-password') }}">Change Password</a>
{% endif %}
{% endif %}
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
......
{% extends "base.html" %}
{% block title %}Change Password - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>Change Password</h1>
<p>Update your account password</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Password Settings</h2>
<form method="POST" action="{{ url_for(request, '/dashboard/change-password') }}">
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" required>
</div>
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" required minlength="6">
</div>
<div class="form-group">
<label for="confirm_password">Confirm New Password</label>
<input type="password" id="confirm_password" name="confirm_password" required minlength="6">
</div>
<div style="margin-top: 1.5rem;">
<button type="submit" class="btn btn-primary">Change Password</button>
</div>
</form>
</div>
</div>
<style>
.card {
background: #16213e;
padding: 2rem;
border-radius: 8px;
margin-top: 1.5rem;
}
.card h2 {
margin-bottom: 1.5rem;
color: #e0e0e0;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #e0e0e0;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #1a1a2e;
color: #e0e0e0;
font-size: 1rem;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
</style>
{% endblock %}
\ No newline at end of file
...@@ -52,5 +52,35 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -52,5 +52,35 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
{% endif %} {% endif %}
</form> </form>
{% if config.oauth2 and (config.oauth2.google.enabled or config.oauth2.github.enabled) %}
<div style="margin: 25px 0; text-align: center; position: relative;">
<hr style="border: none; border-top: 1px solid #ddd; margin: 0;">
<span style="background: white; padding: 0 15px; position: relative; top: -13px; color: #666;">or continue with</span>
</div>
<div style="display: flex; flex-direction: column; gap: 12px;">
{% if config.oauth2.google.enabled %}
<a href="/auth/oauth2/google" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #dadce0; border-radius: 4px; background: white; color: #3c4043; font-weight: 500; text-decoration: none; transition: background 0.2s;">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Continue with Google
</a>
{% endif %}
{% if config.oauth2.github.enabled %}
<a href="/auth/oauth2/github" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #d1d5da; border-radius: 4px; background: #24292e; color: #ffffff; font-weight: 500; text-decoration: none;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Continue with GitHub
</a>
{% endif %}
</div>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}
{% extends "base.html" %}
{% block title %}Edit Profile - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>Edit Profile</h1>
<p>Update your account information</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Account Information</h2>
<form method="POST" action="{{ url_for(request, '/dashboard/profile') }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" value="{{ session.username }}" required>
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" value="{{ session.email }}" required>
</div>
<div class="form-group">
<label>Profile Picture</label>
<div style="display: flex; align-items: center; gap: 1rem;">
<img src="https://www.gravatar.com/avatar/{{ session.email|md5 }}?s=96&d=identicon" alt="Current avatar" style="border-radius: 8px;">
<p style="color: #a0a0a0;">Profile pictures are managed via <a href="https://gravatar.com" target="_blank">Gravatar</a> using your email address</p>
</div>
</div>
<div style="margin-top: 1.5rem;">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
<style>
.card {
background: #16213e;
padding: 2rem;
border-radius: 8px;
margin-top: 1.5rem;
}
.card h2 {
margin-bottom: 1.5rem;
color: #e0e0e0;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #e0e0e0;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #1a1a2e;
color: #e0e0e0;
font-size: 1rem;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
</style>
{% endblock %}
\ No newline at end of file
...@@ -448,6 +448,46 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -448,6 +448,46 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<small style="color: #666; display: block; margin-top: 5px;">How long verification links remain valid (1-168 hours)</small> <small style="color: #666; display: block; margin-top: 5px;">How long verification links remain valid (1-168 hours)</small>
</div> </div>
<h3 style="margin: 30px 0 20px;">OAuth2 Authentication</h3>
<h4 style="margin: 25px 0 15px;">Google OAuth2</h4>
<div class="form-group">
<label>
<input type="checkbox" name="oauth2_google_enabled" {% if config.oauth2 and config.oauth2.google and config.oauth2.google.enabled %}checked{% endif %}>
Enable Google OAuth2 Authentication
</label>
</div>
<div class="form-group">
<label for="oauth2_google_client_id">Google Client ID</label>
<input type="text" id="oauth2_google_client_id" name="oauth2_google_client_id" value="{{ config.oauth2.google.client_id if config.oauth2 and config.oauth2.google else '' }}" placeholder="Client ID from Google Cloud Console">
</div>
<div class="form-group">
<label for="oauth2_google_client_secret">Google Client Secret</label>
<input type="password" id="oauth2_google_client_secret" name="oauth2_google_client_secret" value="{{ config.oauth2.google.client_secret if config.oauth2 and config.oauth2.google else '' }}" placeholder="Client Secret from Google Cloud Console">
</div>
<h4 style="margin: 25px 0 15px;">GitHub OAuth2</h4>
<div class="form-group">
<label>
<input type="checkbox" name="oauth2_github_enabled" {% if config.oauth2 and config.oauth2.github and config.oauth2.github.enabled %}checked{% endif %}>
Enable GitHub OAuth2 Authentication
</label>
</div>
<div class="form-group">
<label for="oauth2_github_client_id">GitHub Client ID</label>
<input type="text" id="oauth2_github_client_id" name="oauth2_github_client_id" value="{{ config.oauth2.github.client_id if config.oauth2 and config.oauth2.github else '' }}" placeholder="Client ID from GitHub Developer Settings">
</div>
<div class="form-group">
<label for="oauth2_github_client_secret">GitHub Client Secret</label>
<input type="password" id="oauth2_github_client_secret" name="oauth2_github_client_secret" value="{{ config.oauth2.github.client_secret if config.oauth2 and config.oauth2.github else '' }}" placeholder="Client Secret from GitHub Developer Settings">
</div>
<h3 style="margin: 30px 0 20px;">SMTP Email Configuration</h3> <h3 style="margin: 30px 0 20px;">SMTP Email Configuration</h3>
<div class="form-group"> <div class="form-group">
......
...@@ -78,6 +78,36 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -78,6 +78,36 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<p>Already have an account? <a href="{{ url_for(request, '/dashboard/login') }}" style="color: #4CAF50;">Login here</a></p> <p>Already have an account? <a href="{{ url_for(request, '/dashboard/login') }}" style="color: #4CAF50;">Login here</a></p>
</div> </div>
</form> </form>
{% if config.oauth2 and (config.oauth2.google.enabled or config.oauth2.github.enabled) %}
<div style="margin: 25px 0; text-align: center; position: relative;">
<hr style="border: none; border-top: 1px solid #ddd; margin: 0;">
<span style="background: white; padding: 0 15px; position: relative; top: -13px; color: #666;">or sign up with</span>
</div>
<div style="display: flex; flex-direction: column; gap: 12px;">
{% if config.oauth2.google.enabled %}
<a href="/auth/oauth2/google" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #dadce0; border-radius: 4px; background: white; color: #3c4043; font-weight: 500; text-decoration: none; transition: background 0.2s;">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Sign up with Google
</a>
{% endif %}
{% if config.oauth2.github.enabled %}
<a href="/auth/oauth2/github" style="display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px; border: 1px solid #d1d5da; border-radius: 4px; background: #24292e; color: #ffffff; font-weight: 500; text-decoration: none;">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Sign up with GitHub
</a>
{% endif %}
</div>
{% endif %}
{% endif %} {% endif %}
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment