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
13b80367
Commit
13b80367
authored
Apr 22, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Payment systems...
parent
946dfd79
Changes
8
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
371 additions
and
230 deletions
+371
-230
database.py
aisbf/database.py
+17
-14
monitor.py
aisbf/payments/crypto/monitor.py
+212
-121
service.py
aisbf/payments/service.py
+15
-19
main.py
main.py
+45
-24
docs.html
templates/dashboard/docs.html
+6
-0
profile.html
templates/dashboard/profile.html
+32
-4
prompts.html
templates/dashboard/prompts.html
+5
-34
wallet.html
templates/dashboard/wallet.html
+39
-14
No files found.
aisbf/database.py
View file @
13b80367
...
@@ -1375,7 +1375,7 @@ class DatabaseManager:
...
@@ -1375,7 +1375,7 @@ class DatabaseManager:
'''
,
(
new_email
,
user_id
))
'''
,
(
new_email
,
user_id
))
conn
.
commit
()
conn
.
commit
()
def
update_user_profile
(
self
,
user_id
:
int
,
username
:
str
,
email
:
str
,
display_name
:
str
=
None
):
def
update_user_profile
(
self
,
user_id
:
int
,
username
:
str
,
email
:
str
,
display_name
:
str
=
None
,
profile_pic
:
str
=
None
):
"""
"""
Update user profile (username and display_name, email is read-only).
Update user profile (username and display_name, email is read-only).
...
@@ -1384,23 +1384,25 @@ class DatabaseManager:
...
@@ -1384,23 +1384,25 @@ class DatabaseManager:
username: New username
username: New username
email: Email (ignored, kept for backward compatibility)
email: Email (ignored, kept for backward compatibility)
display_name: New display name (optional)
display_name: New display name (optional)
profile_pic: Base64-encoded profile picture data URL (optional)
"""
"""
with
self
.
_get_connection
()
as
conn
:
with
self
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
cursor
=
conn
.
cursor
()
placeholder
=
'?'
if
self
.
db_type
==
'sqlite'
else
'
%
s'
placeholder
=
'?'
if
self
.
db_type
==
'sqlite'
else
'
%
s'
fields
=
[
'username = '
+
placeholder
]
params
=
[
username
]
if
display_name
is
not
None
:
if
display_name
is
not
None
:
cursor
.
execute
(
f
'''
fields
.
append
(
'display_name = '
+
placeholder
)
UPDATE users
params
.
append
(
display_name
)
SET username = {placeholder}, display_name = {placeholder}
if
profile_pic
is
not
None
:
WHERE id = {placeholder}
fields
.
append
(
'profile_pic = '
+
placeholder
)
'''
,
(
username
,
display_name
,
user_id
))
params
.
append
(
profile_pic
)
else
:
params
.
append
(
user_id
)
cursor
.
execute
(
f
'''
UPDATE users
cursor
.
execute
(
f
'''
SET username = {placeholder}
UPDATE users SET {', '.join(fields)} WHERE id = {placeholder}
WHERE id = {placeholder}
'''
,
tuple
(
params
))
'''
,
(
username
,
user_id
))
conn
.
commit
()
conn
.
commit
()
def
sanitize_username
(
self
,
input_str
:
str
)
->
str
:
def
sanitize_username
(
self
,
input_str
:
str
)
->
str
:
...
@@ -4315,7 +4317,8 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
...
@@ -4315,7 +4317,8 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
(
'subscription_expires'
,
'TIMESTAMP NULL'
,
'tier_id'
),
(
'subscription_expires'
,
'TIMESTAMP NULL'
,
'tier_id'
),
(
'stripe_customer_id'
,
'VARCHAR(100)'
,
'subscription_expires'
),
(
'stripe_customer_id'
,
'VARCHAR(100)'
,
'subscription_expires'
),
(
'reset_password_token'
,
'VARCHAR(255)'
,
'stripe_customer_id'
),
(
'reset_password_token'
,
'VARCHAR(255)'
,
'stripe_customer_id'
),
(
'reset_password_token_expires'
,
'TIMESTAMP NULL'
,
'reset_password_token'
)
(
'reset_password_token_expires'
,
'TIMESTAMP NULL'
,
'reset_password_token'
),
(
'profile_pic'
,
'TEXT'
,
'reset_password_token_expires'
)
]
]
for
col_name
,
col_def
,
after_col
in
required_columns
:
for
col_name
,
col_def
,
after_col
in
required_columns
:
...
...
aisbf/payments/crypto/monitor.py
View file @
13b80367
This diff is collapsed.
Click to expand it.
aisbf/payments/service.py
View file @
13b80367
...
@@ -55,25 +55,21 @@ class PaymentService:
...
@@ -55,25 +55,21 @@ class PaymentService:
async
def
get_user_crypto_addresses
(
self
,
user_id
:
int
)
->
dict
:
async
def
get_user_crypto_addresses
(
self
,
user_id
:
int
)
->
dict
:
"""Get or create crypto addresses for user"""
"""Get or create crypto addresses for user"""
addresses
=
{}
addresses
=
{}
# Get enabled crypto types
# Get enabled crypto gateways from payment gateway settings
with
self
.
db
.
_get_connection
()
as
conn
:
gateways
=
self
.
db
.
get_payment_gateway_settings
()
cursor
=
conn
.
cursor
()
crypto_types
=
{
'bitcoin'
:
'btc'
,
'ethereum'
:
'eth'
,
'usdt'
:
'usdt'
,
'usdc'
:
'usdc'
}
placeholder
=
'?'
if
self
.
db
.
db_type
==
'sqlite'
else
'
%
s'
cursor
.
execute
(
f
"""
for
gateway_name
,
crypto_type
in
crypto_types
.
items
():
SELECT crypto_type FROM crypto_consolidation_settings
gw
=
gateways
.
get
(
gateway_name
,
{})
WHERE enabled = {placeholder}
if
not
gw
.
get
(
'enabled'
,
False
):
"""
,
(
True
,))
continue
enabled_cryptos
=
cursor
.
fetchall
()
try
:
address
=
await
self
.
wallet_manager
.
get_or_create_user_address
(
user_id
,
crypto_type
)
for
crypto_config
in
enabled_cryptos
:
addresses
[
gateway_name
]
=
address
crypto_type
=
crypto_config
[
0
]
except
Exception
as
e
:
address
=
await
self
.
wallet_manager
.
get_or_create_user_address
(
logger
.
warning
(
f
"Could not get/create {crypto_type} address for user {user_id}: {e}"
)
user_id
,
crypto_type
)
addresses
[
crypto_type
]
=
address
return
addresses
return
addresses
async
def
get_user_wallet_balances
(
self
,
user_id
:
int
)
->
dict
:
async
def
get_user_wallet_balances
(
self
,
user_id
:
int
)
->
dict
:
...
...
main.py
View file @
13b80367
...
@@ -59,6 +59,7 @@ from datetime import datetime, timedelta
...
@@ -59,6 +59,7 @@ from datetime import datetime, timedelta
from
collections
import
defaultdict
from
collections
import
defaultdict
from
pathlib
import
Path
from
pathlib
import
Path
import
json
import
json
import
re
import
markdown
import
markdown
from
urllib.parse
import
urljoin
,
urlencode
from
urllib.parse
import
urljoin
,
urlencode
from
cryptography.fernet
import
Fernet
from
cryptography.fernet
import
Fernet
...
@@ -3198,7 +3199,7 @@ async def dashboard_profile(request: Request):
...
@@ -3198,7 +3199,7 @@ async def dashboard_profile(request: Request):
@
app
.
post
(
"/dashboard/profile"
)
@
app
.
post
(
"/dashboard/profile"
)
async
def
dashboard_profile_save
(
request
:
Request
,
username
:
str
=
Form
(
...
),
display_name
:
str
=
Form
(
""
)):
async
def
dashboard_profile_save
(
request
:
Request
,
username
:
str
=
Form
(
...
),
display_name
:
str
=
Form
(
""
)
,
profile_pic
:
UploadFile
=
File
(
None
)
):
"""Save user profile changes (username and display_name)"""
"""Save user profile changes (username and display_name)"""
auth_check
=
require_dashboard_auth
(
request
)
auth_check
=
require_dashboard_auth
(
request
)
if
isinstance
(
auth_check
,
RedirectResponse
):
if
isinstance
(
auth_check
,
RedirectResponse
):
...
@@ -3208,7 +3209,18 @@ async def dashboard_profile_save(request: Request, username: str = Form(...), di
...
@@ -3208,7 +3209,18 @@ async def dashboard_profile_save(request: Request, username: str = Form(...), di
db
=
DatabaseRegistry
.
get_config_database
()
db
=
DatabaseRegistry
.
get_config_database
()
try
:
try
:
db
.
update_user_profile
(
user_id
,
username
,
None
,
display_name
if
display_name
else
None
)
profile_pic_data
=
None
if
profile_pic
and
profile_pic
.
filename
:
content_type
=
profile_pic
.
content_type
or
''
if
not
content_type
.
startswith
(
'image/'
):
return
RedirectResponse
(
url
=
url_for
(
request
,
"/dashboard/profile?error=Invalid file type. Please upload an image."
),
status_code
=
303
)
data
=
await
profile_pic
.
read
(
1024
*
1024
+
1
)
# read up to 1MB+1 to detect oversized
if
len
(
data
)
>
1024
*
1024
:
return
RedirectResponse
(
url
=
url_for
(
request
,
"/dashboard/profile?error=Image too large. Maximum size is 1MB."
),
status_code
=
303
)
import
base64
profile_pic_data
=
f
"data:{content_type};base64,{base64.b64encode(data).decode()}"
db
.
update_user_profile
(
user_id
,
username
,
None
,
display_name
if
display_name
else
None
,
profile_pic_data
)
# Update session with new username and display_name
# Update session with new username and display_name
request
.
session
[
'username'
]
=
username
request
.
session
[
'username'
]
=
username
request
.
session
[
'display_name'
]
=
display_name
or
''
request
.
session
[
'display_name'
]
=
display_name
or
''
...
@@ -8205,12 +8217,18 @@ async def dashboard_wallet_topup(request: Request):
...
@@ -8205,12 +8217,18 @@ async def dashboard_wallet_topup(request: Request):
if
not
gw
.
get
(
'enabled'
,
False
):
if
not
gw
.
get
(
'enabled'
,
False
):
return
JSONResponse
({
"error"
:
f
"Payment method '{method}' is not enabled"
},
status_code
=
400
)
return
JSONResponse
({
"error"
:
f
"Payment method '{method}' is not enabled"
},
status_code
=
400
)
# Crypto:
return deposit address (manual transfer)
# Crypto:
generate per-user HD wallet address
crypto_methods
=
{
'bitcoin'
,
'ethereum'
,
'usdt'
,
'usdc'
}
crypto_methods
=
{
'bitcoin'
:
'btc'
,
'ethereum'
:
'eth'
,
'usdt'
:
'usdt'
,
'usdc'
:
'usdc'
}
if
method
in
crypto_methods
:
if
method
in
crypto_methods
:
address
=
gw
.
get
(
'address'
,
''
)
crypto_type
=
crypto_methods
[
method
]
if
not
address
:
ps
=
getattr
(
request
.
app
.
state
,
'payment_service'
,
None
)
return
JSONResponse
({
"error"
:
"Crypto address not configured"
},
status_code
=
503
)
if
ps
is
None
:
return
JSONResponse
({
"error"
:
"Payment service unavailable"
},
status_code
=
503
)
try
:
address
=
await
ps
.
wallet_manager
.
get_or_create_user_address
(
user_id
,
crypto_type
)
except
Exception
as
e
:
logger
.
error
(
f
"Crypto address generation error: {e}"
)
return
JSONResponse
({
"error"
:
"Could not generate deposit address"
},
status_code
=
503
)
return
JSONResponse
({
return
JSONResponse
({
"type"
:
"crypto"
,
"type"
:
"crypto"
,
"method"
:
method
,
"method"
:
method
,
...
@@ -8257,22 +8275,14 @@ async def dashboard_wallet_topup(request: Request):
...
@@ -8257,22 +8275,14 @@ async def dashboard_wallet_topup(request: Request):
if
method
==
'paypal'
:
if
method
==
'paypal'
:
try
:
try
:
payment_service
=
getattr
(
request
.
app
.
state
,
'payment_service'
,
None
)
payment_service
=
getattr
(
request
.
app
.
state
,
'payment_service'
,
None
)
if
payment_service
and
hasattr
(
payment_service
,
'paypal_handler'
):
if
not
payment_service
or
not
hasattr
(
payment_service
,
'paypal_handler'
):
from
decimal
import
Decimal
return
JSONResponse
({
"error"
:
"PayPal payment service unavailable"
},
status_code
=
503
)
order
=
await
payment_service
.
paypal_handler
.
create_order
(
from
decimal
import
Decimal
user_id
,
Decimal
(
str
(
amount
)),
metadata
=
{
"type"
:
"wallet_topup"
}
result
=
await
payment_service
.
paypal_handler
.
create_topup_order
(
user_id
,
Decimal
(
str
(
amount
)))
)
if
not
result
.
get
(
'success'
):
return
JSONResponse
({
"type"
:
"paypal"
,
"order_id"
:
order
.
id
})
logger
.
error
(
f
"PayPal top-up error: {result.get('error')}"
)
# Fallback: direct PayPal redirect
return
JSONResponse
({
"error"
:
"PayPal checkout failed. Please try again."
},
status_code
=
502
)
client_id
=
gw
.
get
(
'client_id'
,
''
)
return
JSONResponse
({
"type"
:
"paypal"
,
"approval_url"
:
result
[
'approval_url'
]})
sandbox
=
gw
.
get
(
'sandbox'
,
True
)
paypal_base
=
"https://www.sandbox.paypal.com"
if
sandbox
else
"https://www.paypal.com"
return
JSONResponse
({
"type"
:
"paypal"
,
"paypal_base"
:
paypal_base
,
"client_id"
:
client_id
,
"amount"
:
amount
,
})
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"PayPal top-up error: {e}"
)
logger
.
error
(
f
"PayPal top-up error: {e}"
)
return
JSONResponse
({
"error"
:
"PayPal checkout failed. Please try again."
},
status_code
=
502
)
return
JSONResponse
({
"error"
:
"PayPal checkout failed. Please try again."
},
status_code
=
502
)
...
@@ -8994,7 +9004,7 @@ async def dashboard_docs(request: Request):
...
@@ -8994,7 +9004,7 @@ async def dashboard_docs(request: Request):
# Convert markdown to HTML with extensions for better formatting
# Convert markdown to HTML with extensions for better formatting
html_content
=
markdown
.
markdown
(
html_content
=
markdown
.
markdown
(
markdown_content
,
markdown_content
,
extensions
=
[
'fenced_code'
,
'tables'
,
'nl2br'
,
'sane_lists'
]
extensions
=
[
'fenced_code'
,
'tables'
,
'nl2br'
,
'sane_lists'
,
'toc'
]
)
)
else
:
else
:
html_content
=
"<p>Documentation file not found.</p>"
html_content
=
"<p>Documentation file not found.</p>"
...
@@ -9039,6 +9049,17 @@ async def dashboard_about(request: Request):
...
@@ -9039,6 +9049,17 @@ async def dashboard_about(request: Request):
markdown_content
,
markdown_content
,
extensions
=
[
'fenced_code'
,
'tables'
,
'nl2br'
,
'sane_lists'
]
extensions
=
[
'fenced_code'
,
'tables'
,
'nl2br'
,
'sane_lists'
]
)
)
# Rewrite DOCUMENTATION.md links to /dashboard/docs
html_content
=
re
.
sub
(
r'href="DOCUMENTATION\.md#([^"]*)"'
,
r'href="/dashboard/docs#\1"'
,
html_content
)
html_content
=
re
.
sub
(
r'href="DOCUMENTATION\.md"'
,
'href="/dashboard/docs"'
,
html_content
)
else
:
else
:
html_content
=
"<p>README file not found.</p>"
html_content
=
"<p>README file not found.</p>"
...
...
templates/dashboard/docs.html
View file @
13b80367
...
@@ -135,4 +135,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -135,4 +135,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div
class=
"markdown-content"
>
<div
class=
"markdown-content"
>
{{ content|safe }}
{{ content|safe }}
</div>
</div>
<script>
if
(
location
.
hash
)
{
const
el
=
document
.
querySelector
(
location
.
hash
);
if
(
el
)
el
.
scrollIntoView
();
}
</script>
{% endblock %}
{% endblock %}
templates/dashboard/profile.html
View file @
13b80367
...
@@ -18,7 +18,7 @@
...
@@ -18,7 +18,7 @@
<div
class=
"card"
>
<div
class=
"card"
>
<h2>
Account Information
</h2>
<h2>
Account Information
</h2>
<form
method=
"POST"
action=
"{{ url_for(request, '/dashboard/profile') }}"
>
<form
method=
"POST"
action=
"{{ url_for(request, '/dashboard/profile') }}"
enctype=
"multipart/form-data"
>
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label
for=
"username"
>
Username
</label>
<label
for=
"username"
>
Username
</label>
<input
type=
"text"
id=
"username"
name=
"username"
value=
"{{ session.username }}"
required
>
<input
type=
"text"
id=
"username"
name=
"username"
value=
"{{ session.username }}"
required
>
...
@@ -48,9 +48,22 @@
...
@@ -48,9 +48,22 @@
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label>
Profile Picture
</label>
<label>
Profile Picture
</label>
<div
style=
"display: flex; align-items: center; gap: 1rem;"
>
<div
style=
"display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;"
>
<img
src=
"https://www.gravatar.com/avatar/{{ session.email|md5 }}?s=96&d=identicon"
alt=
"Current avatar"
style=
"border-radius: 8px;"
>
<div
style=
"position: relative; cursor: pointer;"
onclick=
"document.getElementById('profile_pic').click()"
>
<p
style=
"color: #a0a0a0;"
>
Profile pictures are managed via
<a
href=
"https://gravatar.com"
target=
"_blank"
>
Gravatar
</a>
using your email address
</p>
{% if user.profile_pic %}
<img
id=
"avatar-preview"
src=
"{{ user.profile_pic }}"
alt=
"Profile picture"
style=
"width: 96px; height: 96px; border-radius: 8px; object-fit: cover; display: block;"
>
{% else %}
<img
id=
"avatar-preview"
src=
"https://www.gravatar.com/avatar/{{ session.email|md5 }}?s=96&d=identicon"
alt=
"Profile picture"
style=
"width: 96px; height: 96px; border-radius: 8px; object-fit: cover; display: block;"
>
{% endif %}
<div
style=
"position: absolute; inset: 0; background: rgba(0,0,0,0.45); border-radius: 8px; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.2s;"
id=
"avatar-overlay"
>
<span
style=
"color: #fff; font-size: 0.8rem; text-align: center;"
>
Change
</span>
</div>
</div>
<div>
<input
type=
"file"
id=
"profile_pic"
name=
"profile_pic"
accept=
"image/*"
style=
"display: none;"
onchange=
"previewAvatar(this)"
>
<button
type=
"button"
class=
"btn"
style=
"background: #1a1a2e; border: 1px solid #0f3460; color: #e0e0e0;"
onclick=
"document.getElementById('profile_pic').click()"
>
Upload Image
</button>
<small
style=
"color: #a0a0a0; display: block; margin-top: 0.5rem;"
>
Max 1 MB. JPG, PNG, GIF, WebP.
</small>
</div>
</div>
</div>
</div>
</div>
...
@@ -106,4 +119,19 @@
...
@@ -106,4 +119,19 @@
border-color
:
#667eea
;
border-color
:
#667eea
;
}
}
</style>
</style>
<script>
function
previewAvatar
(
input
)
{
if
(
!
input
.
files
||
!
input
.
files
[
0
])
return
;
const
reader
=
new
FileReader
();
reader
.
onload
=
e
=>
{
document
.
getElementById
(
'avatar-preview'
).
src
=
e
.
target
.
result
;
};
reader
.
readAsDataURL
(
input
.
files
[
0
]);
}
const
avatarWrap
=
document
.
querySelector
(
'[onclick="document.getElementById(
\'
profile_pic
\'
).click()"]'
);
const
overlay
=
document
.
getElementById
(
'avatar-overlay'
);
if
(
avatarWrap
&&
overlay
)
{
avatarWrap
.
addEventListener
(
'mouseenter'
,
()
=>
overlay
.
style
.
opacity
=
'1'
);
avatarWrap
.
addEventListener
(
'mouseleave'
,
()
=>
overlay
.
style
.
opacity
=
'0'
);
}
</script>
{% endblock %}
{% endblock %}
\ No newline at end of file
templates/dashboard/prompts.html
View file @
13b80367
...
@@ -31,7 +31,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -31,7 +31,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div
style=
"margin-bottom: 20px;"
>
<div
style=
"margin-bottom: 20px;"
>
<label
style=
"font-weight: 500; margin-bottom: 10px; display: block;"
>
Select Prompt File:
</label>
<label
style=
"font-weight: 500; margin-bottom: 10px; display: block;"
>
Select Prompt File:
</label>
<select
id=
"prompt-selector"
onchange=
"switchPrompt()"
style=
"width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 3px; font-size: 14px;"
>
<select
id=
"prompt-selector"
onchange=
"switchPrompt()"
>
{% for prompt in prompts %}
{% for prompt in prompts %}
<option
value=
"{{ prompt.key }}"
{%
if
loop
.
first
%}
selected
{%
endif
%}
>
{{ prompt.name }}
</option>
<option
value=
"{{ prompt.key }}"
{%
if
loop
.
first
%}
selected
{%
endif
%}
>
{{ prompt.name }}
</option>
{% endfor %}
{% endfor %}
...
@@ -42,9 +42,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -42,9 +42,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input
type=
"hidden"
name=
"prompt_key"
id=
"prompt_key"
value=
""
>
<input
type=
"hidden"
name=
"prompt_key"
id=
"prompt_key"
value=
""
>
<div
class=
"form-group"
>
<div
class=
"form-group"
>
<label
for=
"prompt_content"
style=
"font-weight: 500; margin-bottom: 10px; display: block;"
>
Prompt Content:
</label>
<label
for=
"prompt_content"
>
Prompt Content:
</label>
<textarea
id=
"prompt_content"
name=
"prompt_content"
style=
"
width: 100%; min-height: 400px; padding: 10px; border: 1px solid #ddd; border-radius: 3px; font-family: monospace; font-size: 13px; line-height: 1.5
;"
></textarea>
<textarea
id=
"prompt_content"
name=
"prompt_content"
style=
"
min-height: 400px
;"
></textarea>
<small
style=
"color: #
666
; display: block; margin-top: 5px;"
>
Edit the prompt template. Use markdown formatting as needed.
</small>
<small
style=
"color: #
a0a0a0
; display: block; margin-top: 5px;"
>
Edit the prompt template. Use markdown formatting as needed.
</small>
</div>
</div>
<div
style=
"display: flex; gap: 10px; margin-top: 20px;"
>
<div
style=
"display: flex; gap: 10px; margin-top: 20px;"
>
...
@@ -52,7 +52,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -52,7 +52,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% if not is_admin %}
{% if not is_admin %}
<button
type=
"button"
class=
"btn btn-secondary"
onclick=
"resetPrompt()"
>
Reset to Default
</button>
<button
type=
"button"
class=
"btn btn-secondary"
onclick=
"resetPrompt()"
>
Reset to Default
</button>
{% endif %}
{% endif %}
<a
href=
"
/dashboard
"
class=
"btn btn-secondary"
>
Cancel
</a>
<a
href=
"
{{ url_for(request, '/dashboard') }}
"
class=
"btn btn-secondary"
>
Cancel
</a>
</div>
</div>
</form>
</form>
...
@@ -96,33 +96,4 @@ if (prompts.length > 0) {
...
@@ -96,33 +96,4 @@ if (prompts.length > 0) {
document
.
getElementById
(
'prompt_key'
).
value
=
prompts
[
0
].
key
;
document
.
getElementById
(
'prompt_key'
).
value
=
prompts
[
0
].
key
;
}
}
</script>
</script>
<style>
.form-group
{
margin-bottom
:
20px
;
}
textarea
{
resize
:
vertical
;
}
.btn
{
padding
:
10px
20px
;
border
:
none
;
border-radius
:
3px
;
cursor
:
pointer
;
font-size
:
14px
;
text-decoration
:
none
;
display
:
inline-block
;
}
.btn-secondary
{
background
:
#6c757d
;
color
:
white
;
}
.btn-secondary
:hover
{
background
:
#5a6268
;
}
</style>
{% endblock %}
{% endblock %}
templates/dashboard/wallet.html
View file @
13b80367
...
@@ -94,8 +94,7 @@
...
@@ -94,8 +94,7 @@
<div
style=
"display: flex; flex-wrap: wrap; gap: 8px;"
>
<div
style=
"display: flex; flex-wrap: wrap; gap: 8px;"
>
{% for name in crypto_gateways %}
{% for name in crypto_gateways %}
{% set cfg = enabled_gateways[name] %}
{% set cfg = enabled_gateways[name] %}
{% set address = cfg.get('wallet_address') or cfg.get('address') or '' %}
<button
onclick=
"openCryptoModal('{{ name }}')"
<button
onclick=
"openCryptoModal('{{ name }}','{{ address }}')"
style=
"background:#1a1a2e; border:1px solid #0f3460; color:#e0e0e0; padding:8px 16px; border-radius:6px; cursor:pointer; font-size:13px; font-weight:600; display:flex; align-items:center; gap:6px; transition:border-color .15s;"
style=
"background:#1a1a2e; border:1px solid #0f3460; color:#e0e0e0; padding:8px 16px; border-radius:6px; cursor:pointer; font-size:13px; font-weight:600; display:flex; align-items:center; gap:6px; transition:border-color .15s;"
onmouseover=
"this.style.borderColor='#4a9eff'"
onmouseout=
"this.style.borderColor='#0f3460'"
>
onmouseover=
"this.style.borderColor='#4a9eff'"
onmouseout=
"this.style.borderColor='#0f3460'"
>
{% if name == 'bitcoin' %}
<i
class=
"fab fa-bitcoin"
style=
"color:#f7931a;"
></i>
{% if name == 'bitcoin' %}
<i
class=
"fab fa-bitcoin"
style=
"color:#f7931a;"
></i>
...
@@ -243,9 +242,10 @@
...
@@ -243,9 +242,10 @@
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
// ── Amount buttons ──────────────────────────────────────────
// ── Amount buttons ──────────────────────────────────────────
let
selectedAmount
=
null
;
let
selectedAmount
=
15
;
document
.
querySelectorAll
(
'.amount-btn'
).
forEach
(
btn
=>
{
document
.
querySelectorAll
(
'.amount-btn'
).
forEach
(
btn
=>
{
if
(
parseFloat
(
btn
.
dataset
.
amount
)
===
selectedAmount
)
btn
.
classList
.
add
(
'active'
);
btn
.
addEventListener
(
'click'
,
function
()
{
btn
.
addEventListener
(
'click'
,
function
()
{
document
.
querySelectorAll
(
'.amount-btn'
).
forEach
(
b
=>
b
.
classList
.
remove
(
'active'
));
document
.
querySelectorAll
(
'.amount-btn'
).
forEach
(
b
=>
b
.
classList
.
remove
(
'active'
));
this
.
classList
.
add
(
'active'
);
this
.
classList
.
add
(
'active'
);
...
@@ -423,25 +423,50 @@ function copyAddress(addr, btn) {
...
@@ -423,25 +423,50 @@ function copyAddress(addr, btn) {
let
_cryptoAddr
=
''
;
let
_cryptoAddr
=
''
;
function
openCryptoModal
(
name
,
address
)
{
function
openCryptoModal
(
name
)
{
const
icons
=
{
bitcoin
:
'₿ Bitcoin (BTC)'
,
ethereum
:
'Ξ Ethereum (ETH)'
,
usdt
:
'₮ USDT'
,
usdc
:
'◎ USDC'
};
const
icons
=
{
bitcoin
:
'₿ Bitcoin (BTC)'
,
ethereum
:
'Ξ Ethereum (ETH)'
,
usdt
:
'₮ USDT'
,
usdc
:
'◎ USDC'
};
document
.
getElementById
(
'cryptoModalTitle'
).
textContent
=
icons
[
name
]
||
name
.
toUpperCase
();
document
.
getElementById
(
'cryptoModalTitle'
).
textContent
=
icons
[
name
]
||
name
.
toUpperCase
();
document
.
getElementById
(
'cryptoAddress'
).
textContent
=
address
||
'Address not configured
'
;
document
.
getElementById
(
'cryptoAddress'
).
textContent
=
'Loading…
'
;
_cryptoAddr
=
address
;
_cryptoAddr
=
''
;
const
qrEl
=
document
.
getElementById
(
'cryptoQR'
);
const
qrEl
=
document
.
getElementById
(
'cryptoQR'
);
qrEl
.
innerHTML
=
'<i class="fas fa-spinner fa-spin fa-2x" style="color:#0f3460;"></i>'
;
qrEl
.
innerHTML
=
'<i class="fas fa-spinner fa-spin fa-2x" style="color:#0f3460;"></i>'
;
document
.
getElementById
(
'cryptoModal'
).
classList
.
add
(
'active'
);
document
.
getElementById
(
'cryptoModal'
).
classList
.
add
(
'active'
);
if
(
address
)
{
// Fetch a per-user deposit address from the server
const
qrUrl
=
`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=
${
encodeURIComponent
(
address
)}
&bgcolor=ffffff&color=000000&margin=8`
;
const
amount
=
getAmount
()
||
15
;
const
img
=
new
Image
();
fetch
(
'/dashboard/wallet/topup'
,
{
img
.
onload
=
()
=>
{
qrEl
.
innerHTML
=
''
;
qrEl
.
appendChild
(
img
);
};
method
:
'POST'
,
img
.
onerror
=
()
=>
{
qrEl
.
innerHTML
=
'<span style="color:#888;font-size:12px;">QR unavailable</span>'
;
};
headers
:
{
'Content-Type'
:
'application/json'
},
img
.
src
=
qrUrl
;
body
:
JSON
.
stringify
({
amount
,
payment_method
:
name
})
img
.
style
.
cssText
=
'width:200px;height:200px;border-radius:8px;'
;
})
}
.
then
(
r
=>
r
.
json
())
.
then
(
data
=>
{
if
(
data
.
error
)
{
document
.
getElementById
(
'cryptoAddress'
).
textContent
=
data
.
error
;
qrEl
.
innerHTML
=
'<span style="color:#f87171;font-size:12px;">Error</span>'
;
return
;
}
const
address
=
data
.
address
||
''
;
_cryptoAddr
=
address
;
document
.
getElementById
(
'cryptoAddress'
).
textContent
=
address
||
'Address unavailable'
;
if
(
address
)
{
const
qrUrl
=
`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=
${
encodeURIComponent
(
address
)}
&bgcolor=ffffff&color=000000&margin=8`
;
const
img
=
new
Image
();
img
.
onload
=
()
=>
{
qrEl
.
innerHTML
=
''
;
qrEl
.
appendChild
(
img
);
};
img
.
onerror
=
()
=>
{
qrEl
.
innerHTML
=
'<span style="color:#888;font-size:12px;">QR unavailable</span>'
;
};
img
.
src
=
qrUrl
;
img
.
style
.
cssText
=
'width:200px;height:200px;border-radius:8px;'
;
}
else
{
qrEl
.
innerHTML
=
'<span style="color:#888;font-size:12px;">No address</span>'
;
}
})
.
catch
(()
=>
{
document
.
getElementById
(
'cryptoAddress'
).
textContent
=
'Failed to load address'
;
qrEl
.
innerHTML
=
'<span style="color:#f87171;font-size:12px;">Error</span>'
;
});
}
}
function
closeCryptoModal
()
{
function
closeCryptoModal
()
{
...
...
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