hiddifypanel 10.70.8__py3-none-any.whl → 10.80.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hiddifypanel/VERSION +1 -1
- hiddifypanel/VERSION.py +5 -2
- hiddifypanel/__init__.py +5 -1
- hiddifypanel/apps/__init__.py +0 -0
- hiddifypanel/apps/asgi_app.py +7 -0
- hiddifypanel/apps/celery_app.py +3 -0
- hiddifypanel/apps/wsgi_app.py +5 -0
- hiddifypanel/auth.py +8 -3
- hiddifypanel/base.py +43 -126
- hiddifypanel/base_setup.py +82 -0
- hiddifypanel/cache.py +3 -2
- hiddifypanel/celery.py +45 -0
- hiddifypanel/database.py +7 -0
- hiddifypanel/drivers/ssh_liberty_bridge_api.py +2 -1
- hiddifypanel/drivers/wireguard_api.py +1 -1
- hiddifypanel/hutils/crypto.py +27 -0
- hiddifypanel/hutils/flask.py +5 -3
- hiddifypanel/hutils/network/cf_api.py +5 -5
- hiddifypanel/hutils/proxy/__init__.py +1 -0
- hiddifypanel/hutils/proxy/clash.py +3 -3
- hiddifypanel/hutils/proxy/shared.py +14 -18
- hiddifypanel/hutils/proxy/singbox.py +4 -2
- hiddifypanel/hutils/proxy/wireguard.py +34 -0
- hiddifypanel/hutils/proxy/xray.py +3 -3
- hiddifypanel/hutils/proxy/xrayjson.py +10 -7
- hiddifypanel/models/admin.py +1 -1
- hiddifypanel/models/base_account.py +3 -0
- hiddifypanel/models/config.py +5 -2
- hiddifypanel/models/config_enum.py +15 -2
- hiddifypanel/models/proxy.py +1 -1
- hiddifypanel/models/user.py +2 -2
- hiddifypanel/panel/__init__.py +8 -8
- hiddifypanel/panel/admin/AdminstratorAdmin.py +16 -10
- hiddifypanel/panel/admin/DomainAdmin.py +132 -98
- hiddifypanel/panel/admin/ProxyAdmin.py +4 -0
- hiddifypanel/panel/admin/QuickSetup.py +48 -17
- hiddifypanel/panel/admin/SettingAdmin.py +6 -0
- hiddifypanel/panel/admin/UserAdmin.py +63 -36
- hiddifypanel/panel/admin/adminlte.py +1 -1
- hiddifypanel/panel/admin/templates/index.html +6 -4
- hiddifypanel/panel/admin/templates/model/user_list.html +11 -3
- hiddifypanel/panel/cli.py +14 -3
- hiddifypanel/panel/commercial/restapi/v1/tgbot.py +19 -1
- hiddifypanel/panel/commercial/restapi/v2/admin/system_actions.py +5 -1
- hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +2 -1
- hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +76 -6
- hiddifypanel/panel/common.py +5 -2
- hiddifypanel/panel/common_bp/login.py +14 -8
- hiddifypanel/panel/hlogger.py +32 -0
- hiddifypanel/panel/init_db.py +157 -77
- hiddifypanel/panel/node/__init__.py +9 -0
- hiddifypanel/panel/node/a.py +14 -0
- hiddifypanel/panel/node/hello.py +14 -0
- hiddifypanel/panel/node/test.proto +13 -0
- hiddifypanel/panel/node/test_grpc.py +40 -0
- hiddifypanel/panel/node/test_pb2.py +40 -0
- hiddifypanel/panel/node/test_pb2.pyi +17 -0
- hiddifypanel/panel/node/test_pb2_grpc.py +97 -0
- hiddifypanel/panel/usage.py +13 -3
- hiddifypanel/panel/user/templates/base_singbox_config.json.j2 +16 -0
- hiddifypanel/panel/user/templates/home/home.html +1 -2
- hiddifypanel/panel/user/templates/home/multi.html +1 -2
- hiddifypanel/panel/user/user.py +13 -19
- hiddifypanel/static/apps-icon/singbox.ico +0 -0
- hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/en/LC_MESSAGES/messages.po +125 -30
- hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/fa/LC_MESSAGES/messages.po +123 -32
- hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/pt/LC_MESSAGES/messages.po +114 -22
- hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/ru/LC_MESSAGES/messages.po +129 -32
- hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/zh/LC_MESSAGES/messages.po +107 -18
- hiddifypanel/translations.i18n/en.json +73 -14
- hiddifypanel/translations.i18n/fa.json +72 -13
- hiddifypanel/translations.i18n/fr.json +28 -10
- hiddifypanel/translations.i18n/my.json +1266 -0
- hiddifypanel/translations.i18n/pt.json +68 -9
- hiddifypanel/translations.i18n/ru.json +75 -16
- hiddifypanel/translations.i18n/zh.json +67 -8
- hiddifypanel-10.80.0.dist-info/METADATA +137 -0
- {hiddifypanel-10.70.8.dist-info → hiddifypanel-10.80.0.dist-info}/RECORD +136 -119
- {hiddifypanel-10.70.8.dist-info → hiddifypanel-10.80.0.dist-info}/WHEEL +1 -2
- hiddifypanel-10.80.0.dist-info/entry_points.txt +3 -0
- hiddifypanel-10.70.8.dist-info/METADATA +0 -144
- hiddifypanel-10.70.8.dist-info/entry_points.txt +0 -2
- hiddifypanel-10.70.8.dist-info/top_level.txt +0 -1
- {hiddifypanel-10.70.8.dist-info → hiddifypanel-10.80.0.dist-info}/LICENSE.md +0 -0
@@ -16,6 +16,7 @@ from hiddifypanel.panel import hiddify, custom_widgets
|
|
16
16
|
from .adminlte import AdminLTEModelView
|
17
17
|
from hiddifypanel import hutils
|
18
18
|
|
19
|
+
from loguru import logger
|
19
20
|
from flask import current_app
|
20
21
|
# Define a custom field type for the related domains
|
21
22
|
|
@@ -152,132 +153,165 @@ class DomainAdmin(AdminLTEModelView):
|
|
152
153
|
|
153
154
|
# TODO: refactor this function
|
154
155
|
def on_model_change(self, form, model, is_created):
|
155
|
-
|
156
|
+
# Sanitize domain input
|
157
|
+
model.domain = (model.domain or '').lower().strip()
|
158
|
+
|
159
|
+
# Basic validation
|
156
160
|
if model.domain == '' and model.mode != DomainType.fake:
|
157
161
|
raise ValidationError(_("domain.empty.allowed_for_fake_only"))
|
158
|
-
configs = get_hconfigs()
|
159
|
-
for c in configs:
|
160
|
-
if "domain" in c and c not in [ConfigEnum.decoy_domain, ConfigEnum.reality_fallback_domain] and c.category != 'hidden':
|
161
|
-
if model.domain == configs[c]:
|
162
|
-
raise ValidationError(_("You have used this domain in: ") + _(f"config.{c}.label"))
|
163
|
-
|
164
|
-
for td in Domain.query.filter(Domain.mode == DomainType.reality, Domain.domain != model.domain).all():
|
165
|
-
# print(td)
|
166
|
-
if td.servernames and (model.domain in td.servernames.split(",")):
|
167
|
-
raise ValidationError(_("You have used this domain in: ") + _(f"config.reality_server_names.label") + td.domain)
|
168
|
-
|
169
|
-
if is_created and Domain.query.filter(Domain.domain == model.domain, Domain.child_id == model.child_id).count() > 1:
|
170
|
-
raise ValidationError(_("You have used this domain in: "))
|
171
162
|
|
163
|
+
self._validate_not_used_before(model,is_created)
|
172
164
|
ipv4_list = hutils.network.get_ips(4)
|
173
165
|
ipv6_list = hutils.network.get_ips(6)
|
166
|
+
server_ips = [*ipv4_list, *ipv6_list]
|
174
167
|
|
175
|
-
if not
|
168
|
+
if not server_ips:
|
176
169
|
raise ValidationError(_("Couldn't find your ip addresses"))
|
177
170
|
|
171
|
+
# Validate domain based on mode
|
178
172
|
if "*" in model.domain and model.mode not in [DomainType.cdn, DomainType.auto_cdn_ip]:
|
179
173
|
raise ValidationError(_("Domain can not be resolved! there is a problem in your domain"))
|
180
174
|
|
181
|
-
|
175
|
+
cloudflare_updated=self._update_cloudflare(model, ipv4_list,ipv6_list)
|
176
|
+
|
177
|
+
|
178
|
+
self._validate_domain_ips(model, server_ips)
|
179
|
+
|
180
|
+
# Handle CDN IP settings
|
181
|
+
if model.mode == DomainType.direct and model.cdn_ip:
|
182
|
+
model.cdn_ip = ""
|
183
|
+
raise ValidationError(_("Specifying CDN IP is only valid for CDN mode"))
|
184
|
+
|
185
|
+
if model.mode == DomainType.fake and not model.cdn_ip:
|
186
|
+
model.cdn_ip = str(server_ips[0])
|
187
|
+
|
188
|
+
if model.cdn_ip:
|
189
|
+
try:
|
190
|
+
hutils.network.auto_ip_selector.get_clean_ip(str(model.cdn_ip))
|
191
|
+
except Exception:
|
192
|
+
raise ValidationError(_("Error in auto cdn format"))
|
193
|
+
|
194
|
+
# Update show domains
|
195
|
+
if len(model.show_domains) == Domain.query.count():
|
196
|
+
model.show_domains = []
|
197
|
+
|
198
|
+
# Handle mode-specific settings
|
199
|
+
if model.mode == DomainType.old_xtls_direct and not hconfig(ConfigEnum.xtls_enable):
|
200
|
+
set_hconfig(ConfigEnum.xtls_enable, True)
|
201
|
+
hutils.proxy.get_proxies().invalidate_all()
|
202
|
+
elif model.mode == DomainType.reality:
|
203
|
+
self._validate_reality_settings(model, server_ips)
|
204
|
+
|
205
|
+
# Signal config update if needed
|
206
|
+
old_db_domain = Domain.by_domain(model.domain)
|
207
|
+
if is_created or not old_db_domain or old_db_domain.mode != model.mode:
|
208
|
+
# return hiddify.reinstall_action(complete_install=False, domain_changed=True)
|
209
|
+
hutils.flask.flash_config_success(restart_mode=ApplyMode.apply_config, domain_changed=True)
|
210
|
+
|
211
|
+
|
212
|
+
|
213
|
+
def _update_cloudflare(self, model, ipv4_list,ipv6_list):
|
182
214
|
if hconfig(ConfigEnum.cloudflare) and model.mode not in [DomainType.fake, DomainType.relay, DomainType.reality]:
|
183
215
|
try:
|
184
216
|
proxied = model.mode in [DomainType.cdn, DomainType.auto_cdn_ip]
|
185
|
-
|
217
|
+
if ipv4_list:
|
218
|
+
hutils.network.cf_api.add_or_update_dns_record(model.domain, str(ipv4_list[0]), "A", proxied=proxied)
|
186
219
|
if ipv6_list:
|
187
220
|
hutils.network.cf_api.add_or_update_dns_record(model.domain, str(ipv6_list[0]), "AAAA", proxied=proxied)
|
188
|
-
|
189
|
-
skip_check = True
|
221
|
+
return True
|
190
222
|
except Exception as e:
|
191
223
|
raise ValidationError(__("cloudflare.error") + f' {e}')
|
192
|
-
|
193
|
-
# if model.alias and not model.alias.replace("_", "").isalnum():
|
194
|
-
# hutils.flask.flash(__("Using alias with special charachters may cause problem in some clients like FairVPN."), 'warning')
|
195
|
-
# raise ValidationError(_("You have to add your cloudflare api key to use this feature: "))
|
224
|
+
return False
|
196
225
|
|
197
|
-
|
198
|
-
|
199
|
-
if
|
200
|
-
|
201
|
-
|
202
|
-
elif not skip_check:
|
203
|
-
if not dips:
|
204
|
-
raise ValidationError(_("Domain can not be resolved! there is a problem in your domain")) # type: ignore
|
205
|
-
|
206
|
-
domain_ip_is_same_as_panel = False
|
207
|
-
|
208
|
-
for mip in server_ips:
|
209
|
-
domain_ip_is_same_as_panel |= mip in dips
|
210
|
-
server_ips_str = ', '.join(list(map(str, server_ips)))
|
211
|
-
dips_str = ', '.join(list(map(str, dips)))
|
212
|
-
|
213
|
-
if model.mode == DomainType.direct and not domain_ip_is_same_as_panel:
|
214
|
-
# hutils.flask.flash(__(f"Domain IP={dip} is not matched with your ip={', '.join(list(map(str, ipv4_list)))} which is required in direct mode"),category='error')
|
215
|
-
raise ValidationError(
|
216
|
-
__("Domain IP=%(domain_ip)s is not matched with your ip=%(server_ip)s which is required in direct mode", server_ip=server_ips_str, domain_ip=dips_str)) # type: ignore
|
217
|
-
|
218
|
-
if domain_ip_is_same_as_panel and model.mode in [DomainType.cdn, DomainType.relay, DomainType.fake, DomainType.auto_cdn_ip]:
|
219
|
-
# # hutils.flask.flash(__(f"In CDN mode, Domain IP={dip} should be different to your ip={', '.join(list(map(str, ipv4_list)))}"), 'warning')
|
220
|
-
raise ValidationError(__("In CDN mode, Domain IP=%(domain_ip)s should be different to your ip=%(server_ip)s",
|
221
|
-
server_ip=server_ips_str, domain_ip=dips_str)) # type: ignore
|
222
|
-
|
223
|
-
# if model.mode in [DomainType.ss_faketls, DomainType.telegram_faketls]:
|
224
|
-
# if len(Domain.query.filter(Domain.mode==model.mode and Domain.id!=model.id).all())>0:
|
225
|
-
# ValidationError(f"another {model.mode} is exist")
|
226
|
-
|
227
|
-
model.domain = model.domain.lower()
|
228
|
-
if model.mode == DomainType.direct and model.cdn_ip:
|
229
|
-
model.cdn_ip = ""
|
230
|
-
raise ValidationError(f"Specifying CDN IP is only valid for CDN mode")
|
226
|
+
def _validate_reality_settings(self, model, server_ips):
|
227
|
+
"""Validate REALITY protocol settings with proper error handling"""
|
228
|
+
if not hconfig(ConfigEnum.reality_enable):
|
229
|
+
set_hconfig(ConfigEnum.reality_enable, True)
|
230
|
+
hutils.proxy.get_proxies().invalidate_all()
|
231
231
|
|
232
|
-
|
233
|
-
|
232
|
+
model.servernames = (model.servernames or model.domain).lower().strip()
|
233
|
+
domains_to_check = set()
|
234
|
+
for v in [model.domain, model.servernames]:
|
235
|
+
domains_to_check.update(d.strip() for d in v.split(",") if d.strip())
|
234
236
|
|
235
|
-
|
236
|
-
|
237
|
+
for d in domains_to_check:
|
238
|
+
# Check REALITY compatibility
|
239
|
+
if not hutils.network.is_domain_reality_friendly(d):
|
240
|
+
raise ValidationError(_("Domain is not REALITY friendly!") + f' {d}')
|
237
241
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
set_hconfig(ConfigEnum.xtls_enable, True)
|
244
|
-
hutils.proxy.get_proxies().invalidate_all()
|
245
|
-
elif model.mode == DomainType.reality:
|
246
|
-
if not hconfig(ConfigEnum.reality_enable):
|
247
|
-
set_hconfig(ConfigEnum.reality_enable, True)
|
248
|
-
hutils.proxy.get_proxies().invalidate_all()
|
249
|
-
model.servernames = (model.servernames or model.domain).lower()
|
250
|
-
for v in set([model.domain, model.servernames]):
|
251
|
-
for d in v.split(","):
|
252
|
-
if not d:
|
253
|
-
continue
|
254
|
-
if not hutils.network.is_domain_reality_friendly(d): # the minimum requirement for the REALITY protocol is to have tls1.3 and h2
|
255
|
-
raise ValidationError(_("Domain is not REALITY friendly!") + f' {d}')
|
256
|
-
|
257
|
-
if not hutils.network.is_in_same_asn(d, server_ips[0]):
|
258
|
-
dip = next(iter(dips))
|
242
|
+
try:
|
243
|
+
if not hutils.network.is_in_same_asn(d, server_ips[0]):
|
244
|
+
domain_ips = hutils.network.get_domain_ips(d)
|
245
|
+
if domain_ips:
|
246
|
+
dip = next(iter(domain_ips))
|
259
247
|
server_asn = hutils.network.get_ip_asn(server_ips[0])
|
260
|
-
domain_asn = hutils.network.get_ip_asn(dip)
|
261
|
-
msg = _("domain.reality.asn_issue")
|
262
|
-
|
248
|
+
domain_asn = hutils.network.get_ip_asn(dip)
|
249
|
+
msg = _("domain.reality.asn_issue")
|
250
|
+
if server_asn or domain_asn:
|
251
|
+
msg += f"<br> Server ASN={server_asn}<br>{d}_ASN={domain_asn}"
|
263
252
|
hutils.flask.flash(msg, 'warning')
|
253
|
+
except Exception as e:
|
254
|
+
logger.warning(f"ASN check failed for domain {d}: {str(e)}")
|
264
255
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
256
|
+
# Check fallback compatibility
|
257
|
+
for d in model.servernames.split(","):
|
258
|
+
if d.strip() and not hutils.network.fallback_domain_compatible_with_servernames(model.domain, d):
|
259
|
+
msg = _("REALITY Fallback domain is not compatible with server names!") + f' {d} != {model.domain}'
|
260
|
+
hutils.flask.flash(msg, 'warning')
|
269
261
|
|
270
|
-
if (model.cdn_ip):
|
271
|
-
try:
|
272
|
-
hutils.network.auto_ip_selector.get_clean_ip(str(model.cdn_ip))
|
273
|
-
except BaseException:
|
274
|
-
raise ValidationError(_("Error in auto cdn format"))
|
275
262
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
263
|
+
def _validate_not_used_before(self, model,is_created):
|
264
|
+
configs = get_hconfigs()
|
265
|
+
for c in configs:
|
266
|
+
if "domain" in c and c not in [ConfigEnum.decoy_domain, ConfigEnum.reality_fallback_domain] and c.category != 'hidden':
|
267
|
+
if model.domain == configs[c]:
|
268
|
+
raise ValidationError(_("You have used this domain in: ") + _(f"config.{c}.label"))
|
269
|
+
|
270
|
+
for td in Domain.query.filter(Domain.mode == DomainType.reality, Domain.domain != model.domain).all():
|
271
|
+
# print(td)
|
272
|
+
if td.servernames and (model.domain in td.servernames.split(",")):
|
273
|
+
raise ValidationError(_("You have used this domain in: ") + _(f"config.reality_server_names.label") + td.domain)
|
280
274
|
|
275
|
+
if is_created and Domain.query.filter(Domain.domain == model.domain, Domain.child_id == model.child_id).count() > 1:
|
276
|
+
raise ValidationError(_("You have used this domain in: "))
|
277
|
+
|
278
|
+
def _validate_domain_ips(self, model, server_ips):
|
279
|
+
"""Validate domain IP resolution and matching"""
|
280
|
+
|
281
|
+
# Skip validation for wildcard or empty domains
|
282
|
+
if (model.domain.startswith('*') or not model.domain) and model.mode not in [DomainType.direct]:
|
283
|
+
return True
|
284
|
+
if model.mode in [DomainType.fake, DomainType.reality, DomainType.relay]:
|
285
|
+
return True
|
286
|
+
# Resolve domain IPs with timeout
|
287
|
+
try:
|
288
|
+
dips = hutils.network.get_domain_ips(model.domain)
|
289
|
+
except Exception as e:
|
290
|
+
logger.error(f"Error resolving domain {model.domain}: {str(e)}")
|
291
|
+
raise ValidationError(_("Domain cannot be resolved! Please check DNS settings"))
|
292
|
+
|
293
|
+
# Validate resolution success
|
294
|
+
if not dips:
|
295
|
+
raise ValidationError(_("Domain cannot be resolved! Please check DNS settings"))
|
296
|
+
|
297
|
+
# Check IP matching based on mode
|
298
|
+
domain_ip_matches_server = any(ip in dips for ip in server_ips)
|
299
|
+
server_ips_str = ', '.join(map(str, server_ips))
|
300
|
+
dips_str = ', '.join(map(str, dips))
|
301
|
+
|
302
|
+
if not domain_ip_matches_server and model.mode in [DomainType.direct]:
|
303
|
+
raise ValidationError(
|
304
|
+
__("Domain IP=%(domain_ip)s is not matched with your ip=%(server_ip)s which is required in direct mode",
|
305
|
+
server_ip=server_ips_str, domain_ip=dips_str))
|
306
|
+
|
307
|
+
if domain_ip_matches_server and model.mode in [DomainType.cdn, DomainType.relay, DomainType.fake, DomainType.auto_cdn_ip]:
|
308
|
+
raise ValidationError(
|
309
|
+
__("In CDN mode, Domain IP=%(domain_ip)s should be different to your ip=%(server_ip)s",
|
310
|
+
server_ip=server_ips_str, domain_ip=dips_str))
|
311
|
+
|
312
|
+
return True
|
313
|
+
|
314
|
+
|
281
315
|
# def after_model_change(self,form, model, is_created):
|
282
316
|
# if model.show_domains.count==0:
|
283
317
|
# db.session.bulk_save_objects(ShowDomain(model.id,model.id))
|
@@ -96,6 +96,8 @@ def get_all_proxy_form(empty=False):
|
|
96
96
|
|
97
97
|
for cdn in categories1:
|
98
98
|
class CDNForm(FlaskForm):
|
99
|
+
class Meta:
|
100
|
+
csrf = False
|
99
101
|
pass
|
100
102
|
cdn_proxies = [c for c in proxies if c.cdn == cdn]
|
101
103
|
pgroup = {
|
@@ -107,6 +109,8 @@ def get_all_proxy_form(empty=False):
|
|
107
109
|
protos = sorted([c for c in {pgroup.get(c.proto, c.proto): 1 for c in cdn_proxies}])
|
108
110
|
for proto in protos:
|
109
111
|
class ProtoForm(FlaskForm):
|
112
|
+
class Meta:
|
113
|
+
csrf = False
|
110
114
|
pass
|
111
115
|
proto_proxies = [c for c in cdn_proxies if pgroup.get(c.proto, c.proto) == proto]
|
112
116
|
for proxy in proto_proxies:
|
@@ -10,7 +10,7 @@ from flask_wtf import FlaskForm
|
|
10
10
|
from flask_bootstrap import SwitchField
|
11
11
|
from hiddifypanel.panel import hiddify
|
12
12
|
from flask_classful import FlaskView
|
13
|
-
from wtforms.validators import ValidationError
|
13
|
+
from wtforms.validators import ValidationError, Length, InputRequired
|
14
14
|
# from gettext import gettext as _
|
15
15
|
|
16
16
|
from hiddifypanel.models import Domain, DomainType, StrConfig, ConfigEnum, get_hconfigs
|
@@ -28,12 +28,14 @@ class QuickSetup(FlaskView):
|
|
28
28
|
if next:
|
29
29
|
step = step + 1
|
30
30
|
form = {1: get_lang_form,
|
31
|
-
2:
|
32
|
-
3:
|
31
|
+
2: get_password_form,
|
32
|
+
3: get_quick_setup_form,
|
33
|
+
4: get_proxy_form}
|
33
34
|
|
34
35
|
return form[step](empty=empty or next)
|
35
36
|
|
36
37
|
def index(self):
|
38
|
+
|
37
39
|
return render_template(
|
38
40
|
'quick_setup.html',
|
39
41
|
form=self.current_form(),
|
@@ -45,11 +47,12 @@ class QuickSetup(FlaskView):
|
|
45
47
|
def post(self):
|
46
48
|
if request.args.get('changepw') == "true":
|
47
49
|
AdminUser.current_admin_or_owner().uuid = str(uuid.uuid4())
|
50
|
+
# AdminUser.current_admin_or_owner().password = hutils.random.get_random_password()
|
48
51
|
db.session.commit()
|
49
52
|
|
50
53
|
set_hconfig(ConfigEnum.first_setup, False)
|
51
54
|
form = self.current_form()
|
52
|
-
if not form.validate_on_submit() or form.step.data not in ["1", "2", "3"]:
|
55
|
+
if not form.validate_on_submit() or form.step.data not in ["1", "2", "3","4"]:
|
53
56
|
hutils.flask.flash(_('config.validation-error'), 'danger')
|
54
57
|
return render_template(
|
55
58
|
'quick_setup.html', form=form,
|
@@ -69,7 +72,7 @@ def get_lang_form(empty=False):
|
|
69
72
|
default=hconfig(ConfigEnum.admin_lang))
|
70
73
|
# lang=wtf.SelectField(_("config.lang.label"),choices=[("en",_("lang.en")),("fa",_("lang.fa"))],description=_("config.lang.description"),default=hconfig(ConfigEnum.lang))
|
71
74
|
country = wtf.SelectField(
|
72
|
-
_("config.country.label"), choices=[("ir", _("Iran")), ("zh", _("China")), ("other", "Others")],
|
75
|
+
_("config.country.label"), choices=[("ir", _("Iran")), ("zh", _("China")), ("ru", _("Russia")), ("other", "Others")],
|
73
76
|
description=_("config.country.description"),
|
74
77
|
default=hconfig(ConfigEnum.country))
|
75
78
|
lang_submit = wtf.SubmitField(_('Submit'))
|
@@ -84,9 +87,9 @@ def get_lang_form(empty=False):
|
|
84
87
|
|
85
88
|
return render_template(
|
86
89
|
'quick_setup.html', form=view.current_form(next=True),
|
87
|
-
admin_link=admin_link(),
|
88
|
-
ipv4=hutils.network.get_ip_str(4),
|
89
|
-
ipv6=hutils.network.get_ip_str(6),
|
90
|
+
# admin_link=admin_link(),
|
91
|
+
# ipv4=hutils.network.get_ip_str(4),
|
92
|
+
# ipv6=hutils.network.get_ip_str(6),
|
90
93
|
show_domain_info=False)
|
91
94
|
|
92
95
|
form = LangForm(None)if empty else LangForm()
|
@@ -94,6 +97,34 @@ def get_lang_form(empty=False):
|
|
94
97
|
return form
|
95
98
|
|
96
99
|
|
100
|
+
def get_password_form(empty=False):
|
101
|
+
class PasswordForm(FlaskForm):
|
102
|
+
step = wtf.HiddenField(default="1")
|
103
|
+
admin_pass = wtf.PasswordField(
|
104
|
+
_("user.password.title"),
|
105
|
+
description=_("user.password.description"),
|
106
|
+
default="",validators=[
|
107
|
+
|
108
|
+
InputRequired(message=_("user.password.validation-required")),
|
109
|
+
Length(min=8, message=_("user.password.validation-lenght"))
|
110
|
+
|
111
|
+
])
|
112
|
+
password_submit = wtf.SubmitField(_('Submit'))
|
113
|
+
|
114
|
+
def post(self, view):
|
115
|
+
AdminUser.current_admin_or_owner().update_password(self.admin_pass.data)
|
116
|
+
|
117
|
+
return render_template(
|
118
|
+
'quick_setup.html', form=view.current_form(next=True),
|
119
|
+
admin_link=admin_link(),
|
120
|
+
ipv4=hutils.network.get_ip_str(4),
|
121
|
+
ipv6=hutils.network.get_ip_str(6),
|
122
|
+
show_domain_info=False)
|
123
|
+
|
124
|
+
form = PasswordForm(None)if empty else PasswordForm()
|
125
|
+
form.step.data = "2"
|
126
|
+
return form
|
127
|
+
|
97
128
|
def get_proxy_form(empty=False):
|
98
129
|
class ProxyForm(FlaskForm):
|
99
130
|
step = wtf.HiddenField(default="3")
|
@@ -127,7 +158,7 @@ def get_proxy_form(empty=False):
|
|
127
158
|
setattr(ProxyForm, f'{cf.key}', field)
|
128
159
|
setattr(ProxyForm, "submit_global", wtf.fields.SubmitField(_('Submit')))
|
129
160
|
form = ProxyForm(None) if empty else ProxyForm()
|
130
|
-
form.step.data = "
|
161
|
+
form.step.data = "4"
|
131
162
|
return form
|
132
163
|
|
133
164
|
|
@@ -210,7 +241,7 @@ def get_quick_setup_form(empty=False):
|
|
210
241
|
show_domain_info=False)
|
211
242
|
|
212
243
|
form = BasicConfigs(None) if empty else BasicConfigs()
|
213
|
-
form.step.data = "
|
244
|
+
form.step.data = "3"
|
214
245
|
return form
|
215
246
|
|
216
247
|
|
@@ -220,11 +251,11 @@ def validate_domain(form, field):
|
|
220
251
|
if dip is None:
|
221
252
|
raise ValidationError(_("Domain can not be resolved! there is a problem in your domain"))
|
222
253
|
|
223
|
-
|
224
|
-
|
225
|
-
if dip
|
254
|
+
myips = hutils.network.get_ips()
|
255
|
+
# Fixed: Changed from get_ip(4) to get_ip(6)
|
256
|
+
if dip not in myips:
|
226
257
|
raise ValidationError(_("Domain (%(domain)s)-> IP=%(domain_ip)s is not matched with your ip=%(server_ip)s which is required in direct mode",
|
227
|
-
server_ip=
|
258
|
+
server_ip=myips, domain_ip=dip, domain=domain))
|
228
259
|
|
229
260
|
|
230
261
|
def validate_domain_cdn(form, field):
|
@@ -235,10 +266,10 @@ def validate_domain_cdn(form, field):
|
|
235
266
|
if dip is None:
|
236
267
|
raise ValidationError(_("Domain can not be resolved! there is a problem in your domain"))
|
237
268
|
|
238
|
-
|
239
|
-
if
|
269
|
+
myips = hutils.network.get_ips()
|
270
|
+
if dip in myips:
|
240
271
|
raise ValidationError(_("In CDN mode, Domain IP=%(domain_ip)s should be different to your ip=%(server_ip)s",
|
241
|
-
server_ip=
|
272
|
+
server_ip=myips, domain_ip=dip, domain=domain))
|
242
273
|
|
243
274
|
|
244
275
|
def admin_link():
|
@@ -137,6 +137,10 @@ class SettingAdmin(FlaskView):
|
|
137
137
|
form = get_config_form()
|
138
138
|
else:
|
139
139
|
hutils.flask.flash(_('config.validation-error'), 'danger') # type: ignore
|
140
|
+
for field, errors in form.errors.items():
|
141
|
+
for error in errors:
|
142
|
+
hutils.flask.flash(error, 'danger') # type: ignore
|
143
|
+
|
140
144
|
|
141
145
|
return reset_action or render_template('config.html', form=form)
|
142
146
|
|
@@ -185,6 +189,8 @@ def get_config_form():
|
|
185
189
|
continue
|
186
190
|
|
187
191
|
class CategoryForm(FlaskForm):
|
192
|
+
class Meta:
|
193
|
+
csrf = False
|
188
194
|
description_for_fieldset = wtf.TextAreaField("", description=_(f'config.{cat}.description'), render_kw={"class": "d-none"})
|
189
195
|
for c2 in cat_configs:
|
190
196
|
if not (c2 in configs_key):
|
@@ -27,7 +27,7 @@ from hiddifypanel import hutils
|
|
27
27
|
class UserAdmin(AdminLTEModelView):
|
28
28
|
column_default_sort = ('id', False) # Sort by username in ascending order
|
29
29
|
|
30
|
-
column_sortable_list = ["is_active", "name", "current_usage", 'mode', "remaining_days", "comment", 'last_online', "uuid"
|
30
|
+
column_sortable_list = ["is_active", "name", "current_usage", 'mode', "remaining_days", "max_ips", "comment", 'last_online', "uuid"]
|
31
31
|
column_searchable_list = ["uuid", "name"]
|
32
32
|
column_list = ["is_active", "name", "UserLinks", "current_usage", "remaining_days", "comment", 'last_online', 'mode', 'admin', "uuid"]
|
33
33
|
column_editable_list = ["comment", "name", "uuid"]
|
@@ -37,8 +37,8 @@ class UserAdmin(AdminLTEModelView):
|
|
37
37
|
# 'disable_user': SwitchField(_("Disable User"))
|
38
38
|
}
|
39
39
|
list_template = 'model/user_list.html'
|
40
|
-
|
41
|
-
form_columns = ["name",
|
40
|
+
# "max_ips",
|
41
|
+
form_columns = ["name","comment", "usage_limit", "reset_usage", "package_days", "reset_days", "mode", "uuid", "enable"]
|
42
42
|
# form_excluded_columns = ['current_usage', 'monthly', 'telegram_id', 'last_online', 'expiry_time', 'last_reset_time', 'current_usage_GB',
|
43
43
|
# 'start_date', 'added_by', 'admin', 'details', 'max_ips', 'ed25519_private_key', 'ed25519_public_key', 'username', 'password']
|
44
44
|
page_size = 50
|
@@ -48,7 +48,6 @@ class UserAdmin(AdminLTEModelView):
|
|
48
48
|
# can_export = True
|
49
49
|
# form_overrides = dict(monthly=SwitchField)
|
50
50
|
form_overrides = {
|
51
|
-
|
52
51
|
'start_date': custom_widgets.DaysLeftField,
|
53
52
|
'mode': custom_widgets.EnumSelectField,
|
54
53
|
'usage_limit': custom_widgets.UsageField
|
@@ -71,7 +70,8 @@ class UserAdmin(AdminLTEModelView):
|
|
71
70
|
'validators': [Regexp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', message=__("Should be a valid uuid"))]
|
72
71
|
# 'label': 'First Name',
|
73
72
|
# 'validators': [required()]
|
74
|
-
}
|
73
|
+
},
|
74
|
+
|
75
75
|
# ,
|
76
76
|
# 'expiry_time':{
|
77
77
|
# "":'%Y-%m-%d'
|
@@ -131,7 +131,7 @@ class UserAdmin(AdminLTEModelView):
|
|
131
131
|
if model.is_active:
|
132
132
|
link = '<i class="fa-solid fa-circle-check text-success"></i> '
|
133
133
|
elif len(model.devices):
|
134
|
-
link = '<i class="fa-solid fa-users-slash text-danger" title="{_("Too many Connected IPs")}"></i>'
|
134
|
+
link = f'<i class="fa-solid fa-users-slash text-danger" title="{_("Too many Connected IPs")}"></i>'
|
135
135
|
else:
|
136
136
|
link = '<i class="fa-solid fa-circle-xmark text-danger"></i> '
|
137
137
|
|
@@ -161,8 +161,8 @@ class UserAdmin(AdminLTEModelView):
|
|
161
161
|
|
162
162
|
def _usage_formatter(view, context, model, name):
|
163
163
|
u = round(model.current_usage_GB, 3)
|
164
|
-
t = round(model.usage_limit_GB, 3)
|
165
|
-
rate = round(u * 100 /
|
164
|
+
t = max(round(model.usage_limit_GB, 3), 0.001) # Prevent division by zero
|
165
|
+
rate = min(round(u * 100 / t), 100) # Cap at 100%
|
166
166
|
state = "danger" if u >= t else ('warning' if rate > 80 else 'success')
|
167
167
|
color = "#ff7e7e" if u >= t else ('#ffc107' if rate > 80 else '#9ee150')
|
168
168
|
return Markup(f"""
|
@@ -225,36 +225,45 @@ class UserAdmin(AdminLTEModelView):
|
|
225
225
|
|
226
226
|
def on_form_prefill(self, form, id=None):
|
227
227
|
# print("================",form._obj.start_date)
|
228
|
-
if
|
228
|
+
if form._obj is None:
|
229
|
+
return
|
230
|
+
|
231
|
+
if id is None or form._obj.start_date is None or form._obj.current_usage==0:
|
229
232
|
msg = _("Package not started yet.")
|
230
233
|
# form.reset['class']="d-none"
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
+
if form._obj.start_date is None:
|
235
|
+
if hasattr(form, 'reset_days'):
|
236
|
+
delattr(form, 'reset_days')
|
234
237
|
else:
|
235
|
-
remaining = form._obj.remaining_days
|
238
|
+
remaining = form._obj.remaining_days
|
236
239
|
relative_remaining = hutils.convert.format_timedelta(datetime.timedelta(days=remaining))
|
237
240
|
msg = _("Remaining about %(relative)s, exactly %(days)s days", relative=relative_remaining, days=remaining)
|
238
241
|
form.reset_days.label.text += f" ({msg})"
|
239
|
-
usr_usage = f" ({_('user.home.usage.title')} {round(form._obj.current_usage_GB,3)}GB)"
|
240
|
-
form.reset_usage.label.text += usr_usage
|
241
|
-
form.reset_usage.data = False
|
242
242
|
form.reset_days.data = False
|
243
243
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
form.
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
244
|
+
# Handle reset_usage field
|
245
|
+
if form._obj.current_usage == 0:
|
246
|
+
if hasattr(form, 'reset_usage'):
|
247
|
+
delattr(form, 'reset_usage')
|
248
|
+
else:
|
249
|
+
usr_usage = f" ({_('user.home.usage.title')} {round(form._obj.current_usage_GB,3)}GB)"
|
250
|
+
if hasattr(form, 'reset_usage'):
|
251
|
+
form.reset_usage.label.text += usr_usage
|
252
|
+
form.reset_usage.data = False
|
253
|
+
|
254
|
+
if hasattr(form, 'usage_limit'):
|
255
|
+
form.usage_limit.label.text += usr_usage
|
256
|
+
|
257
|
+
# Handle package days info
|
258
|
+
if form._obj.start_date and hasattr(form, 'package_days'):
|
259
|
+
started = form._obj.start_date - datetime.date.today()
|
260
|
+
msg = _("Started from %(relative)s", relative=hutils.convert.format_timedelta(started))
|
261
|
+
form.package_days.label.text += f" ({msg})"
|
262
|
+
if started.days <= 0:
|
263
|
+
exact_start = _("Started %(days)s days ago", days=-started.days)
|
264
|
+
else:
|
265
|
+
exact_start = _("Will Start in %(days)s days", days=started.days)
|
266
|
+
form.package_days.description += f" ({exact_start})"
|
258
267
|
|
259
268
|
def get_edit_form(self):
|
260
269
|
form = super().get_edit_form()
|
@@ -265,26 +274,44 @@ class UserAdmin(AdminLTEModelView):
|
|
265
274
|
return form
|
266
275
|
|
267
276
|
def on_model_change(self, form, model, is_created):
|
268
|
-
|
277
|
+
# Validate max_ips
|
278
|
+
try:
|
279
|
+
model.max_ips = max(3, min(int(model.max_ips or 10000), 10000))
|
280
|
+
except (ValueError, TypeError):
|
281
|
+
model.max_ips = 1000
|
282
|
+
|
283
|
+
# Show donation message
|
269
284
|
if len(User.query.all()) % 4 == 0:
|
270
285
|
hutils.flask.flash(('<div id="show-modal-donation"></div>'), ' d-none')
|
286
|
+
|
287
|
+
# Validate UUID
|
271
288
|
if not re.match("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", model.uuid):
|
272
289
|
raise ValidationError('Invalid UUID e.g.,' + str(uuid.uuid4()))
|
290
|
+
|
291
|
+
# Handle reset flags
|
273
292
|
if hasattr(form, 'reset_usage') and form.reset_usage.data:
|
274
293
|
model.current_usage_GB = 0
|
275
|
-
|
276
|
-
# raise ValidationError('Invalid Telegram ID')
|
277
|
-
# if form.disable_user.data:
|
278
|
-
# model.mode=UserMode.disable
|
294
|
+
|
279
295
|
if hasattr(form, 'reset_days') and form.reset_days.data:
|
280
296
|
model.start_date = None
|
281
|
-
|
297
|
+
|
298
|
+
# Validate package days
|
299
|
+
try:
|
300
|
+
model.package_days = min(int(model.package_days), 10000)
|
301
|
+
except (ValueError, TypeError):
|
302
|
+
model.package_days = 10000
|
303
|
+
|
304
|
+
# Handle user ownership
|
282
305
|
old_user = User.by_id(model.id)
|
283
306
|
if not model.added_by or model.added_by == 1:
|
284
307
|
model.added_by = g.account.id
|
308
|
+
|
309
|
+
# Validate user limits
|
285
310
|
if not g.account.can_have_more_users():
|
286
311
|
raise ValidationError(_('You have too much users! You can have only %(active)s active users and %(total)s users',
|
287
312
|
active=g.account.max_active_users, total=g.account.max_users))
|
313
|
+
|
314
|
+
# Handle UUID changes
|
288
315
|
if old_user and old_user.uuid != model.uuid:
|
289
316
|
user_driver.remove_client(old_user)
|
290
317
|
|
@@ -3,6 +3,7 @@ from hiddifypanel import auth
|
|
3
3
|
from flask_admin.form import SecureForm
|
4
4
|
|
5
5
|
|
6
|
+
|
6
7
|
class AdminLTEModelView(ModelView):
|
7
8
|
form_base_class = SecureForm
|
8
9
|
edit_modal = True
|
@@ -17,6 +18,5 @@ class AdminLTEModelView(ModelView):
|
|
17
18
|
edit_modal_template = 'flask-admin/model/modals/edit.html'
|
18
19
|
details_modal_template = 'flask-admin/model/modals/details.html'
|
19
20
|
|
20
|
-
# form_base_class = SecureForm
|
21
21
|
def inaccessible_callback(self, name, **kwargs):
|
22
22
|
return auth.redirect_to_login() # type: ignore
|