Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
A
aisbf
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nexlab
aisbf
Commits
2b07302d
Commit
2b07302d
authored
Apr 20, 2026
by
Your Name
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Providers for users ok
parent
d39326be
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
306 additions
and
114 deletions
+306
-114
claude.py
aisbf/auth/claude.py
+30
-7
codex.py
aisbf/auth/codex.py
+25
-5
kilo.py
aisbf/auth/kilo.py
+24
-5
qwen.py
aisbf/auth/qwen.py
+26
-6
claude.py
aisbf/providers/claude.py
+23
-12
codex.py
aisbf/providers/codex.py
+26
-8
kilo.py
aisbf/providers/kilo.py
+29
-13
qwen.py
aisbf/providers/qwen.py
+52
-13
main.py
main.py
+70
-43
aisbf-oauth2-extension.zip
static/aisbf-oauth2-extension.zip
+0
-0
user_providers.html
templates/dashboard/user_providers.html
+1
-2
No files found.
aisbf/auth/claude.py
View file @
2b07302d
...
...
@@ -101,26 +101,31 @@ class ClaudeAuth:
REDIRECT_URI
=
DEFAULT_REDIRECT_URI
CLI_USER_AGENT
=
CLI_USER_AGENT
def
__init__
(
self
,
credentials_file
:
Optional
[
str
]
=
None
,
redirect_uri
:
Optional
[
str
]
=
None
):
def
__init__
(
self
,
credentials_file
:
Optional
[
str
]
=
None
,
redirect_uri
:
Optional
[
str
]
=
None
,
skip_initial_load
:
bool
=
False
,
save_callback
:
Optional
[
callable
]
=
None
):
"""
Initialize Claude authentication.
Args:
credentials_file: Path to credentials file (default: ~/.aisbf/claude_credentials.json)
skip_initial_load: If True, do not load credentials from file on initialization
save_callback: Optional callback to save credentials instead of writing to file
"""
if
credentials_file
:
self
.
credentials_file
=
Path
(
credentials_file
)
.
expanduser
()
else
:
# Store credentials in ~/.aisbf/ directory (AISBF config directory)
self
.
credentials_file
=
Path
.
home
()
/
".aisbf"
/
"claude_credentials.json"
# Allow overriding redirect URI for reverse proxy deployments
self
.
redirect_uri
=
redirect_uri
if
redirect_uri
is
not
None
else
DEFAULT_REDIRECT_URI
self
.
tokens
=
self
.
_load_credentials
()
self
.
tokens
=
None
self
.
_save_callback
=
save_callback
if
not
skip_initial_load
:
self
.
tokens
=
self
.
_load_credentials
()
self
.
_oauth_state
=
None
# Store state for OAuth flow
self
.
_code_verifier
=
None
# Store verifier for OAuth flow
# Log TLS fingerprinting capability
if
HAS_CURL_CFFI
:
logger
.
info
(
f
"ClaudeAuth initialized with TLS fingerprinting (curl_cffi) - credentials: {self.credentials_file}"
)
...
...
@@ -142,7 +147,25 @@ class ClaudeAuth:
return
None
def
_save_credentials
(
self
,
data
:
Dict
):
"""Save credentials to file with file locking to prevent race conditions."""
"""
Save credentials:
- If save_callback is provided, use it (database save for user providers)
- Otherwise, save to file with file locking to prevent race conditions
"""
self
.
tokens
=
data
if
self
.
_save_callback
:
# User provider: ONLY use callback, NO file fallback EVER
try
:
self
.
_save_callback
({
'tokens'
:
data
})
logger
.
info
(
"ClaudeAuth: Saved credentials via callback"
)
return
except
Exception
as
e
:
logger
.
error
(
f
"ClaudeAuth: Failed to save credentials to database: {e}"
)
# DO NOT FALLBACK TO FILE SAVE FOR REGULAR USERS
raise
# Admin/global provider ONLY: save to file
try
:
self
.
tokens
=
data
# Store id_token if received (contains account info)
...
...
aisbf/auth/codex.py
View file @
2b07302d
...
...
@@ -54,13 +54,15 @@ class CodexOAuth2:
Supports authentication with OpenAI's Codex OAuth2 endpoints.
"""
def
__init__
(
self
,
credentials_file
:
Optional
[
str
]
=
None
,
issuer
:
Optional
[
str
]
=
None
):
def
__init__
(
self
,
credentials_file
:
Optional
[
str
]
=
None
,
issuer
:
Optional
[
str
]
=
None
,
skip_initial_load
:
bool
=
False
,
save_callback
:
Optional
[
callable
]
=
None
):
"""
Initialize Codex OAuth2 client.
Args:
credentials_file: Path to credentials JSON file (default: ~/.aisbf/codex_credentials.json)
issuer: OAuth2 issuer URL (default: https://auth.openai.com)
skip_initial_load: If True, do not load credentials from file on initialization
save_callback: Optional callback to save credentials instead of writing to file
"""
# Expand and resolve path immediately to absolute path
default_path
=
os
.
path
.
expanduser
(
"~/.aisbf/codex_credentials.json"
)
...
...
@@ -71,10 +73,12 @@ class CodexOAuth2:
self
.
credentials_file
=
os
.
path
.
abspath
(
expanded
)
else
:
self
.
credentials_file
=
default_path
self
.
issuer
=
(
issuer
or
DEFAULT_ISSUER
)
.
rstrip
(
"/"
)
self
.
credentials
=
None
self
.
_load_credentials
()
self
.
_save_callback
=
save_callback
if
not
skip_initial_load
:
self
.
_load_credentials
()
def
_load_credentials
(
self
)
->
None
:
"""Load credentials from file if it exists."""
...
...
@@ -89,11 +93,27 @@ class CodexOAuth2:
def
_save_credentials
(
self
,
credentials
:
Dict
[
str
,
Any
])
->
None
:
"""
Save credentials to file with secure permissions.
Save credentials:
- If save_callback is provided, use it (database save for user providers)
- Otherwise, save to file with secure permissions (admin/global providers)
Args:
credentials: Credentials dict to save
"""
self
.
credentials
=
credentials
if
self
.
_save_callback
:
# User provider: ONLY use callback, NO file fallback EVER
try
:
self
.
_save_callback
(
credentials
)
logger
.
info
(
f
"CodexOAuth2: Saved credentials via callback"
)
return
except
Exception
as
e
:
logger
.
error
(
f
"CodexOAuth2: Failed to save credentials to database: {e}"
)
# DO NOT FALLBACK TO FILE SAVE FOR REGULAR USERS
raise
# Admin/global provider ONLY: save to file
try
:
# Path is already expanded and absolute from __init__
resolved_path
=
self
.
credentials_file
...
...
aisbf/auth/kilo.py
View file @
2b07302d
...
...
@@ -40,18 +40,21 @@ class KiloOAuth2:
Supports authentication with Kilo Gateway at https://api.kilo.ai.
"""
def
__init__
(
self
,
credentials_file
:
Optional
[
str
]
=
None
,
api_base
:
Optional
[
str
]
=
None
):
def
__init__
(
self
,
credentials_file
:
Optional
[
str
]
=
None
,
api_base
:
Optional
[
str
]
=
None
,
skip_initial_load
:
bool
=
False
,
save_callback
:
Optional
[
callable
]
=
None
):
"""
Initialize Kilo OAuth2 client.
Args:
credentials_file: Path to credentials JSON file (default: ~/.kilo_credentials.json)
api_base: Base URL for Kilo API (default: https://api.kilo.ai)
skip_initial_load: If True, do not load credentials from file on initialization
"""
self
.
credentials_file
=
os
.
path
.
expanduser
(
credentials_file
)
if
credentials_file
else
os
.
path
.
expanduser
(
"~/.kilo_credentials.json"
)
self
.
api_base
=
api_base
or
os
.
environ
.
get
(
"KILO_API_URL"
,
"https://api.kilo.ai"
)
self
.
credentials
=
None
self
.
_load_credentials
()
self
.
_save_callback
=
save_callback
if
not
skip_initial_load
:
self
.
_load_credentials
()
def
_load_credentials
(
self
)
->
None
:
"""Load credentials from file if it exists."""
...
...
@@ -66,17 +69,33 @@ class KiloOAuth2:
def
_save_credentials
(
self
,
credentials
:
Dict
[
str
,
Any
])
->
None
:
"""
Save credentials to file with secure permissions.
Save credentials:
- If save_callback is provided, use it (database save for user providers)
- Otherwise, save to file with secure permissions (admin/global providers)
Args:
credentials: Credentials dict to save
"""
self
.
credentials
=
credentials
if
self
.
_save_callback
:
# User provider: ONLY use callback, NO file fallback EVER
try
:
self
.
_save_callback
(
credentials
)
logger
.
info
(
f
"KiloOAuth2: Saved credentials via callback"
)
return
except
Exception
as
e
:
logger
.
error
(
f
"KiloOAuth2: Failed to save credentials to database: {e}"
)
# DO NOT FALLBACK TO FILE SAVE FOR REGULAR USERS
raise
# Admin/global provider ONLY: save to file
try
:
# Ensure directory exists
cred_dir
=
os
.
path
.
dirname
(
self
.
credentials_file
)
if
cred_dir
:
# Only create if there's a directory component
os
.
makedirs
(
cred_dir
,
exist_ok
=
True
)
# Write credentials
with
open
(
self
.
credentials_file
,
'w'
)
as
f
:
json
.
dump
(
credentials
,
f
,
indent
=
2
)
...
...
aisbf/auth/qwen.py
View file @
2b07302d
...
...
@@ -96,15 +96,17 @@ class QwenOAuth2:
Supports authentication with Qwen's OAuth2 endpoints and automatic token refresh.
"""
def
__init__
(
self
,
credentials_file
:
Optional
[
str
]
=
None
):
def
__init__
(
self
,
credentials_file
:
Optional
[
str
]
=
None
,
skip_initial_load
:
bool
=
False
,
save_callback
:
Optional
[
callable
]
=
None
):
"""
Initialize Qwen OAuth2 client.
⚠️ WARNING: OAuth2 authentication for Qwen has been discontinued.
This client will not work with DashScope API. Use API key authentication instead.
Args:
credentials_file: Path to credentials JSON file (default: ~/.aisbf/qwen_credentials.json)
skip_initial_load: If True, do not load credentials from file on initialization
save_callback: Optional callback to save credentials instead of writing to file
"""
logger
.
warning
(
"⚠️ Qwen OAuth2 service has been discontinued by Qwen. "
...
...
@@ -116,7 +118,9 @@ class QwenOAuth2:
self
.
credentials
=
None
self
.
_file_mod_time
=
0
self
.
_last_check
=
0
self
.
_load_credentials
()
self
.
_save_callback
=
save_callback
if
not
skip_initial_load
:
self
.
_load_credentials
()
def
_load_credentials
(
self
)
->
None
:
"""Load credentials from file if it exists."""
...
...
@@ -136,11 +140,27 @@ class QwenOAuth2:
def
_save_credentials
(
self
,
credentials
:
Dict
[
str
,
Any
])
->
None
:
"""
Save credentials to file with secure permissions and file locking.
Save credentials:
- If save_callback is provided, use it (database save for user providers)
- Otherwise, save to file with secure permissions and file locking (admin/global providers)
Args:
credentials: Credentials dict to save
"""
self
.
credentials
=
credentials
if
self
.
_save_callback
:
# User provider: ONLY use callback, NO file fallback EVER
try
:
self
.
_save_callback
(
credentials
)
logger
.
info
(
f
"QwenOAuth2: Saved credentials via callback"
)
return
except
Exception
as
e
:
logger
.
error
(
f
"QwenOAuth2: Failed to save credentials to database: {e}"
)
# DO NOT FALLBACK TO FILE SAVE FOR REGULAR USERS
raise
# Admin/global provider ONLY: save to file
try
:
# Ensure directory exists
os
.
makedirs
(
os
.
path
.
dirname
(
self
.
credentials_file
),
exist_ok
=
True
)
...
...
aisbf/providers/claude.py
View file @
2b07302d
...
...
@@ -102,12 +102,21 @@ class ClaudeProviderHandler(BaseProviderHandler):
def
_load_auth_from_db
(
self
,
provider_id
:
str
,
credentials_file
:
str
):
"""
Load OAuth2 credentials from database for non-admin users.
Falls back to file-based credentials if not found in database.
Load OAuth2 credentials:
- Admin users (user_id=None): ONLY load from file
- Regular users: ONLY load from database, NO file fallback
"""
from
..auth.claude
import
ClaudeAuth
import
logging
if
self
.
user_id
is
None
:
# Admin user: ONLY use file-based credentials
logging
.
getLogger
(
__name__
)
.
info
(
f
"ClaudeProviderHandler: Admin user, loading credentials from file: {credentials_file}"
)
return
ClaudeAuth
(
credentials_file
=
credentials_file
)
# Regular user: ONLY use database credentials, NO file fallback
try
:
from
..database
import
get_database
from
..auth.claude
import
ClaudeAuth
db
=
get_database
()
if
db
:
db_creds
=
db
.
get_user_oauth2_credentials
(
...
...
@@ -116,22 +125,24 @@ class ClaudeProviderHandler(BaseProviderHandler):
auth_type
=
'claude_oauth2'
)
if
db_creds
and
db_creds
.
get
(
'credentials'
):
# Create auth instance with database credentials
auth
=
ClaudeAuth
(
credentials_file
=
credentials_file
)
# Override the loaded credentials with database credentials
# Create auth instance with skip_initial_load=True to avoid file read
# Pass save callback to save credentials back to database
auth
=
ClaudeAuth
(
credentials_file
=
credentials_file
,
skip_initial_load
=
True
,
save_callback
=
lambda
creds
:
self
.
_save_auth_to_db
(
creds
)
)
# Set tokens directly from database
auth
.
tokens
=
db_creds
[
'credentials'
]
.
get
(
'tokens'
,
{})
import
logging
logging
.
getLogger
(
__name__
)
.
info
(
f
"ClaudeProviderHandler: Loaded credentials from database for user {self.user_id}"
)
return
auth
except
Exception
as
e
:
import
logging
logging
.
getLogger
(
__name__
)
.
warning
(
f
"ClaudeProviderHandler: Failed to load credentials from database: {e}"
)
# Fall back to file-based credentials
from
..auth.claude
import
ClaudeAuth
import
logging
logging
.
getLogger
(
__name__
)
.
info
(
f
"ClaudeProviderHandler: Falling back to file-based credentials for user {self.user_id}"
)
return
ClaudeAuth
(
credentials_file
=
credentials_file
)
# For regular users, NO file fallback - return empty auth instance
logging
.
getLogger
(
__name__
)
.
info
(
f
"ClaudeProviderHandler: No database credentials found for user {self.user_id}, returning unauthenticated instance"
)
return
ClaudeAuth
(
credentials_file
=
credentials_file
,
skip_initial_load
=
True
)
def
_init_session_identifiers
(
self
):
"""Initialize persistent session identifiers (device_id, account_uuid, session_id)."""
...
...
aisbf/providers/codex.py
View file @
2b07302d
...
...
@@ -112,9 +112,22 @@ class CodexProviderHandler(BaseProviderHandler):
def
_load_oauth2_from_db
(
self
,
provider_id
:
str
,
credentials_file
:
str
,
issuer
:
str
)
->
CodexOAuth2
:
"""
Load OAuth2 credentials from database for non-admin users.
Falls back to file-based credentials if not found in database.
Load OAuth2 credentials:
- Admin users (user_id=None): ONLY load from file
- Regular users: ONLY load from database, NO file fallback
"""
from
..auth.codex
import
CodexOAuth2
import
logging
if
self
.
user_id
is
None
:
# Admin user: ONLY use file-based credentials
logging
.
getLogger
(
__name__
)
.
info
(
f
"CodexProviderHandler: Admin user, loading credentials from file: {credentials_file}"
)
return
CodexOAuth2
(
credentials_file
=
credentials_file
,
issuer
=
issuer
,
)
# Regular user: ONLY use database credentials, NO file fallback
try
:
from
..database
import
get_database
db
=
get_database
()
...
...
@@ -125,23 +138,28 @@ class CodexProviderHandler(BaseProviderHandler):
auth_type
=
'codex_oauth2'
)
if
db_creds
and
db_creds
.
get
(
'credentials'
):
# Create OAuth2 instance with database credentials
# Create OAuth2 instance with skip_initial_load=True to avoid file read
# Pass save callback to save credentials back to database
oauth2
=
CodexOAuth2
(
credentials_file
=
credentials_file
,
issuer
=
issuer
,
skip_initial_load
=
True
,
save_callback
=
lambda
creds
:
self
.
_save_oauth2_to_db
(
creds
)
)
#
Override the loaded credentials with database credentials
#
Set credentials directly from database
oauth2
.
credentials
=
db_creds
[
'credentials'
]
logg
er
.
info
(
f
"CodexProviderHandler: Loaded credentials from database for user {self.user_id}"
)
logg
ing
.
getLogger
(
__name__
)
.
info
(
f
"CodexProviderHandler: Loaded credentials from database for user {self.user_id}"
)
return
oauth2
except
Exception
as
e
:
logg
er
.
warning
(
f
"CodexProviderHandler: Failed to load credentials from database: {e}"
)
logg
ing
.
getLogger
(
__name__
)
.
warning
(
f
"CodexProviderHandler: Failed to load credentials from database: {e}"
)
# F
all back to file-based credentials
logg
er
.
info
(
f
"CodexProviderHandler: Falling back to file-based credentials for user {self.user_id}
"
)
# F
or regular users, NO file fallback - return empty auth instance
logg
ing
.
getLogger
(
__name__
)
.
info
(
f
"CodexProviderHandler: No database credentials found for user {self.user_id}, returning unauthenticated instance
"
)
return
CodexOAuth2
(
credentials_file
=
credentials_file
,
issuer
=
issuer
,
skip_initial_load
=
True
,
save_callback
=
lambda
creds
:
self
.
_save_oauth2_to_db
(
creds
)
)
async
def
_get_valid_api_key
(
self
)
->
str
:
...
...
aisbf/providers/kilo.py
View file @
2b07302d
...
...
@@ -110,12 +110,21 @@ class KiloProviderHandler(BaseProviderHandler):
def
_load_oauth2_from_db
(
self
,
provider_id
:
str
,
credentials_file
:
str
,
api_base
:
str
):
"""
Load OAuth2 credentials from database for non-admin users.
Falls back to file-based credentials if not found in database.
Load OAuth2 credentials:
- Admin users (user_id=None): ONLY load from file
- Regular users: ONLY load from database, NO file fallback
"""
from
..auth.kilo
import
KiloOAuth2
import
logging
if
self
.
user_id
is
None
:
# Admin user: ONLY use file-based credentials
logging
.
getLogger
(
__name__
)
.
info
(
f
"KiloProviderHandler: Admin user, loading credentials from file: {credentials_file}"
)
return
KiloOAuth2
(
credentials_file
=
credentials_file
,
api_base
=
api_base
)
# Regular user: ONLY use database credentials, NO file fallback
try
:
from
..database
import
get_database
from
..auth.kilo
import
KiloOAuth2
db
=
get_database
()
if
db
:
db_creds
=
db
.
get_user_oauth2_credentials
(
...
...
@@ -124,22 +133,29 @@ class KiloProviderHandler(BaseProviderHandler):
auth_type
=
'kilo_oauth2'
)
if
db_creds
and
db_creds
.
get
(
'credentials'
):
# Create OAuth2 instance with database credentials
oauth2
=
KiloOAuth2
(
credentials_file
=
credentials_file
,
api_base
=
api_base
)
# Override the loaded credentials with database credentials
# Create OAuth2 instance with skip_initial_load=True to avoid file read
# Pass save callback to save credentials back to database
oauth2
=
KiloOAuth2
(
credentials_file
=
credentials_file
,
api_base
=
api_base
,
skip_initial_load
=
True
,
save_callback
=
lambda
creds
:
self
.
_save_oauth2_to_db
(
creds
)
)
# Set credentials directly from database
oauth2
.
credentials
=
db_creds
[
'credentials'
]
import
logging
logging
.
getLogger
(
__name__
)
.
info
(
f
"KiloProviderHandler: Loaded credentials from database for user {self.user_id}"
)
return
oauth2
except
Exception
as
e
:
import
logging
logging
.
getLogger
(
__name__
)
.
warning
(
f
"KiloProviderHandler: Failed to load credentials from database: {e}"
)
# Fall back to file-based credentials
from
..auth.kilo
import
KiloOAuth2
import
logging
logging
.
getLogger
(
__name__
)
.
info
(
f
"KiloProviderHandler: Falling back to file-based credentials for user {self.user_id}"
)
return
KiloOAuth2
(
credentials_file
=
credentials_file
,
api_base
=
api_base
)
# For regular users, NO file fallback - return empty auth instance
logging
.
getLogger
(
__name__
)
.
info
(
f
"KiloProviderHandler: No database credentials found for user {self.user_id}, returning unauthenticated instance"
)
return
KiloOAuth2
(
credentials_file
=
credentials_file
,
api_base
=
api_base
,
skip_initial_load
=
True
,
save_callback
=
lambda
creds
:
self
.
_save_oauth2_to_db
(
creds
)
)
def
_save_oauth2_to_db
(
self
,
credentials
:
Dict
)
->
None
:
"""
...
...
aisbf/providers/qwen.py
View file @
2b07302d
...
...
@@ -98,12 +98,21 @@ class QwenProviderHandler(BaseProviderHandler):
def
_load_auth_from_db
(
self
,
provider_id
:
str
,
credentials_file
:
str
):
"""
Load OAuth2 credentials from database for non-admin users.
Falls back to file-based credentials if not found in database.
Load OAuth2 credentials:
- Admin users (user_id=None): ONLY load from file
- Regular users: ONLY load from database, NO file fallback
"""
from
..auth.qwen
import
QwenOAuth2
import
logging
if
self
.
user_id
is
None
:
# Admin user: ONLY use file-based credentials
logging
.
getLogger
(
__name__
)
.
info
(
f
"QwenProviderHandler: Admin user, loading credentials from file: {credentials_file}"
)
return
QwenOAuth2
(
credentials_file
=
credentials_file
)
# Regular user: ONLY use database credentials, NO file fallback
try
:
from
..database
import
get_database
from
..auth.qwen
import
QwenOAuth2
db
=
get_database
()
if
db
:
db_creds
=
db
.
get_user_oauth2_credentials
(
...
...
@@ -112,22 +121,52 @@ class QwenProviderHandler(BaseProviderHandler):
auth_type
=
'qwen_oauth2'
)
if
db_creds
and
db_creds
.
get
(
'credentials'
):
# Create auth instance with database credentials
auth
=
QwenOAuth2
(
credentials_file
=
credentials_file
)
# Override the loaded credentials with database credentials
# Create auth instance with skip_initial_load=True to avoid file read
# Pass save callback to save credentials back to database
auth
=
QwenOAuth2
(
credentials_file
=
credentials_file
,
skip_initial_load
=
True
,
save_callback
=
lambda
creds
:
self
.
_save_auth_to_db
(
creds
)
)
# Set credentials directly from database
auth
.
credentials
=
db_creds
[
'credentials'
]
import
logging
logging
.
getLogger
(
__name__
)
.
info
(
f
"QwenProviderHandler: Loaded credentials from database for user {self.user_id}"
)
return
auth
except
Exception
as
e
:
import
logging
logging
.
getLogger
(
__name__
)
.
warning
(
f
"QwenProviderHandler: Failed to load credentials from database: {e}"
)
# Fall back to file-based credentials
from
..auth.qwen
import
QwenOAuth2
import
logging
logging
.
getLogger
(
__name__
)
.
info
(
f
"QwenProviderHandler: Falling back to file-based credentials for user {self.user_id}"
)
return
QwenOAuth2
(
credentials_file
=
credentials_file
)
# For regular users, NO file fallback - return empty auth instance
logging
.
getLogger
(
__name__
)
.
info
(
f
"QwenProviderHandler: No database credentials found for user {self.user_id}, returning unauthenticated instance"
)
return
QwenOAuth2
(
credentials_file
=
credentials_file
,
skip_initial_load
=
True
,
save_callback
=
lambda
creds
:
self
.
_save_auth_to_db
(
creds
)
)
def
_save_auth_to_db
(
self
,
credentials
:
Dict
)
->
None
:
"""
Save OAuth2 credentials to database for non-admin users.
This is called after successful device flow authentication.
"""
if
self
.
user_id
is
None
:
# Admin user uses file-based credentials, nothing to save to DB
return
try
:
from
..database
import
get_database
db
=
get_database
()
if
db
:
db
.
save_user_oauth2_credentials
(
user_id
=
self
.
user_id
,
provider_id
=
self
.
provider_id
,
auth_type
=
'qwen_oauth2'
,
credentials
=
credentials
)
import
logging
logging
.
getLogger
(
__name__
)
.
info
(
f
"QwenProviderHandler: Saved credentials to database for user {self.user_id}"
)
except
Exception
as
e
:
import
logging
logging
.
getLogger
(
__name__
)
.
warning
(
f
"QwenProviderHandler: Failed to save credentials to database: {e}"
)
async
def
_get_sdk_client
(
self
):
"""Get or create an OpenAI SDK client configured with authentication (OAuth2 or API key)."""
...
...
main.py
View file @
2b07302d
...
...
@@ -3786,10 +3786,8 @@ async def dashboard_providers(request: Request):
db
=
DatabaseRegistry
.
get_config_database
()
user_providers
=
db
.
get_user_providers
(
current_user_id
)
# Convert to the format expected by the frontend
providers_data
=
{}
for
provider
in
user_providers
:
providers_data
[
provider
[
'provider_id'
]]
=
provider
[
'config'
]
# Always pass raw user providers format to the template (array)
providers_data
=
user_providers
# Check for success parameter
success
=
request
.
query_params
.
get
(
'success'
)
...
...
@@ -4020,9 +4018,6 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
)
else
:
success_msg
=
"Configuration saved successfully!"
from
aisbf.database
import
get_database
db
=
DatabaseRegistry
.
get_config_database
()
user_providers
=
db
.
get_user_providers
(
current_user_id
)
return
templates
.
TemplateResponse
(
request
=
request
,
...
...
@@ -4031,7 +4026,7 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
"request"
:
request
,
"session"
:
request
.
session
,
"__version__"
:
__version__
,
"user_providers_json"
:
json
.
dumps
(
user_providers
),
"user_providers_json"
:
json
.
dumps
(
providers_data
),
"user_id"
:
current_user_id
,
"success"
:
success_msg
}
...
...
@@ -4101,20 +4096,19 @@ async def dashboard_providers_get_models(request: Request):
"error"
:
"provider_key is required"
},
status_code
=
400
)
# Check if provider exists in config
if
not
config
or
provider_key
not
in
config
.
providers
:
return
JSONResponse
({
"success"
:
False
,
"error"
:
f
"Provider '{provider_key}' not found in configuration"
},
status_code
=
404
)
# Get user ID from session
current_user_id
=
request
.
session
.
get
(
'user_id'
)
# Get provider handler
# Get provider handler
- pass user_id to automatically handle user-specific providers
from
aisbf.providers
import
get_provider_handler
provider_config
=
config
.
providers
[
provider_key
]
api_key
=
provider_config
.
api_key
if
hasattr
(
provider_config
,
'api_key'
)
else
None
handler
=
get_provider_handler
(
provider_key
,
api_key
)
try
:
handler
=
get_provider_handler
(
provider_key
,
user_id
=
current_user_id
)
except
ValueError
as
e
:
return
JSONResponse
({
"success"
:
False
,
"error"
:
str
(
e
)
},
status_code
=
404
)
# Fetch models from provider
models_result
=
await
handler
.
get_models
()
...
...
@@ -6131,14 +6125,28 @@ async def dashboard_provider_file_delete(
@
app
.
get
(
"/dashboard/providers/{provider_name}/auth/check"
)
async
def
dashboard_provider_auth_check
(
request
:
Request
,
provider_name
:
str
):
"""Check OAuth authentication status for a provider"""
auth_check
=
require_
admin
(
request
)
auth_check
=
require_
dashboard_auth
(
request
)
if
auth_check
:
return
auth_check
try
:
# Load current provider configuration
config
=
Config
()
provider_config
=
config
.
providers
.
get
(
provider_name
)
# Get user ID from session
current_user_id
=
request
.
session
.
get
(
'user_id'
)
# Load provider configuration
provider_config
=
None
if
current_user_id
is
None
:
# Admin: check global config
global_config
=
Config
()
provider_config
=
global_config
.
providers
.
get
(
provider_name
)
else
:
# Regular user: get from user providers
from
aisbf.database
import
DatabaseRegistry
db
=
DatabaseRegistry
.
get_config_database
()
user_provider
=
db
.
get_user_provider
(
current_user_id
,
provider_name
)
if
user_provider
:
provider_config
=
user_provider
[
'config'
]
if
not
provider_config
:
return
JSONResponse
(
...
...
@@ -6146,11 +6154,19 @@ async def dashboard_provider_auth_check(request: Request, provider_name: str):
content
=
{
"authenticated"
:
False
,
"error"
:
f
"Provider '{provider_name}' not found"
}
)
provider_type
=
provider_config
.
type
# Handle both dict (user providers) and object (global providers)
if
isinstance
(
provider_config
,
dict
):
provider_type
=
provider_config
.
get
(
'type'
)
else
:
provider_type
=
provider_config
.
type
if
provider_type
==
'claude'
:
from
aisbf.auth.claude
import
ClaudeAuth
claude_config
=
provider_config
.
claude_config
or
{}
# Handle dict vs object
if
isinstance
(
provider_config
,
dict
):
claude_config
=
provider_config
.
get
(
'claude_config'
,
{})
else
:
claude_config
=
provider_config
.
claude_config
or
{}
auth
=
ClaudeAuth
(
credentials_file
=
claude_config
.
get
(
'credentials_file'
,
'~/.claude_credentials.json'
))
is_auth
=
auth
.
is_authenticated
()
result
=
{
"authenticated"
:
is_auth
}
...
...
@@ -6160,7 +6176,11 @@ async def dashboard_provider_auth_check(request: Request, provider_name: str):
elif
provider_type
==
'kilocode'
:
from
aisbf.auth.kilo
import
KiloOAuth2
kilo_config
=
provider_config
.
kilo_config
or
{}
# Handle dict vs object
if
isinstance
(
provider_config
,
dict
):
kilo_config
=
provider_config
.
get
(
'kilo_config'
,
{})
else
:
kilo_config
=
provider_config
.
kilo_config
or
{}
auth
=
KiloOAuth2
(
credentials_file
=
kilo_config
.
get
(
'credentials_file'
,
'~/.kilo_credentials.json'
))
is_auth
=
auth
.
is_authenticated
()
result
=
{
"authenticated"
:
is_auth
}
...
...
@@ -6172,7 +6192,11 @@ async def dashboard_provider_auth_check(request: Request, provider_name: str):
elif
provider_type
==
'qwen'
:
from
aisbf.auth.qwen
import
QwenOAuth2
qwen_config
=
provider_config
.
qwen_config
or
{}
# Handle dict vs object
if
isinstance
(
provider_config
,
dict
):
qwen_config
=
provider_config
.
get
(
'qwen_config'
,
{})
else
:
qwen_config
=
provider_config
.
qwen_config
or
{}
auth
=
QwenOAuth2
(
credentials_file
=
qwen_config
.
get
(
'credentials_file'
,
'~/.aisbf/qwen_credentials.json'
))
is_auth
=
auth
.
is_authenticated
()
result
=
{
"authenticated"
:
is_auth
}
...
...
@@ -6185,7 +6209,11 @@ async def dashboard_provider_auth_check(request: Request, provider_name: str):
elif
provider_type
==
'codex'
:
from
aisbf.auth.codex
import
CodexOAuth2
codex_config
=
provider_config
.
codex_config
or
{}
# Handle dict vs object
if
isinstance
(
provider_config
,
dict
):
codex_config
=
provider_config
.
get
(
'codex_config'
,
{})
else
:
codex_config
=
provider_config
.
codex_config
or
{}
auth
=
CodexOAuth2
(
credentials_file
=
codex_config
.
get
(
'credentials_file'
,
'~/.aisbf/codex_credentials.json'
))
is_auth
=
auth
.
is_authenticated
()
result
=
{
"authenticated"
:
is_auth
}
...
...
@@ -6388,8 +6416,9 @@ async def dashboard_user_tokens_delete(request: Request, token_id: int):
try
:
db
.
delete_user_api_token
(
user_id
,
token_id
)
}
)
return
JSONResponse
(
content
=
{
"success"
:
True
})
except
Exception
as
e
:
return
JSONResponse
(
status_code
=
500
,
content
=
{
"error"
:
str
(
e
)})
@
app
.
get
(
"/dashboard/response-cache/stats"
)
async
def
dashboard_response_cache_stats
(
request
:
Request
):
...
...
@@ -12047,9 +12076,7 @@ async def dashboard_claude_auth_start(request: Request):
from
aisbf.auth.claude
import
ClaudeAuth
# Create auth instance
auth
=
ClaudeAuth
()
# Override credentials file if specified
auth
.
credentials_file
=
Path
(
credentials_file
)
.
expanduser
()
auth
=
ClaudeAuth
(
credentials_file
=
credentials_file
,
skip_initial_load
=
True
)
# Generate PKCE challenge
verifier
,
challenge
=
auth
.
_generate_pkce
()
...
...
@@ -12366,7 +12393,7 @@ async def dashboard_kilo_auth_start(request: Request):
from
aisbf.auth.kilo
import
KiloOAuth2
# Create auth instance
auth
=
KiloOAuth2
(
credentials_file
=
credentials_file
)
auth
=
KiloOAuth2
(
credentials_file
=
credentials_file
,
skip_initial_load
=
True
)
# Initiate device authorization (async method)
device_auth
=
await
auth
.
initiate_device_auth
()
...
...
@@ -12437,7 +12464,7 @@ async def dashboard_kilo_auth_poll(request: Request):
from
aisbf.auth.kilo
import
KiloOAuth2
# Create auth instance
auth
=
KiloOAuth2
(
credentials_file
=
credentials_file
)
auth
=
KiloOAuth2
(
credentials_file
=
credentials_file
,
skip_initial_load
=
True
)
# Poll device authorization status (async method)
result
=
await
auth
.
poll_device_auth
(
device_code
)
...
...
@@ -12645,7 +12672,7 @@ async def dashboard_kilo_auth_logout(request: Request):
from
aisbf.auth.kilo
import
KiloOAuth2
# Create auth instance
auth
=
KiloOAuth2
(
credentials_file
=
credentials_file
)
auth
=
KiloOAuth2
(
credentials_file
=
credentials_file
,
skip_initial_load
=
True
)
# Logout (clear credentials)
auth
.
logout
()
...
...
@@ -12687,8 +12714,8 @@ async def dashboard_codex_auth_start(request: Request):
from
aisbf.auth.codex
import
CodexOAuth2
# Create auth instance
auth
=
CodexOAuth2
(
credentials_file
=
credentials_file
,
issuer
=
issuer
)
auth
=
CodexOAuth2
(
credentials_file
=
credentials_file
,
issuer
=
issuer
,
skip_initial_load
=
True
)
# Request device code (returns immediately)
device_info
=
await
auth
.
request_device_code_flow
()
...
...
@@ -12759,7 +12786,7 @@ async def dashboard_codex_auth_poll(request: Request):
from
aisbf.auth.codex
import
CodexOAuth2
# Create auth instance
auth
=
CodexOAuth2
(
credentials_file
=
credentials_file
,
issuer
=
issuer
)
auth
=
CodexOAuth2
(
credentials_file
=
credentials_file
,
issuer
=
issuer
,
skip_initial_load
=
True
)
# Set device auth info on the instance (required for poll_device_code_completion)
auth
.
_device_auth_id
=
device_auth_id
...
...
@@ -13046,10 +13073,10 @@ async def dashboard_qwen_auth_start(request: Request):
# Import QwenOAuth2
from
aisbf.auth.qwen
import
QwenOAuth2
# Create auth instance
auth
=
QwenOAuth2
(
credentials_file
=
credentials_file
)
auth
=
QwenOAuth2
(
credentials_file
=
credentials_file
,
skip_initial_load
=
True
)
logger
.
info
(
f
"QwenOAuth2: Requesting device code for provider: {provider_key}"
)
# Request device code
...
...
@@ -13125,7 +13152,7 @@ async def dashboard_qwen_auth_poll(request: Request):
from
aisbf.auth.qwen
import
QwenOAuth2
# Create auth instance
auth
=
QwenOAuth2
(
credentials_file
=
credentials_file
)
auth
=
QwenOAuth2
(
credentials_file
=
credentials_file
,
skip_initial_load
=
True
)
# Poll for token - returns token dict if approved, None if still pending
result
=
await
auth
.
poll_device_token
(
device_code
,
code_verifier
)
...
...
static/aisbf-oauth2-extension.zip
View file @
2b07302d
No preview for this file type
templates/dashboard/user_providers.html
View file @
2b07302d
...
...
@@ -134,6 +134,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block extra_js %}
<script>
let
providersData
=
{};
let
expandedProviders
=
new
Set
();
let
rawProviders
=
{{
user_providers_json
|
safe
}};
// Convert user providers format to the format expected by the UI
...
...
@@ -141,8 +142,6 @@ rawProviders.forEach(provider => {
providersData
[
provider
.
provider_id
]
=
provider
.
config
;
});
let
expandedProviders
=
new
Set
();
// Chunk size: 512KB chunks for maximum compatibility with restrictive proxies
const
CHUNK_SIZE
=
512
*
1024
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment