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.
Files changed (89) hide show
  1. hiddifypanel/VERSION +1 -1
  2. hiddifypanel/VERSION.py +5 -2
  3. hiddifypanel/__init__.py +5 -1
  4. hiddifypanel/apps/__init__.py +0 -0
  5. hiddifypanel/apps/asgi_app.py +7 -0
  6. hiddifypanel/apps/celery_app.py +3 -0
  7. hiddifypanel/apps/wsgi_app.py +5 -0
  8. hiddifypanel/auth.py +8 -3
  9. hiddifypanel/base.py +43 -126
  10. hiddifypanel/base_setup.py +82 -0
  11. hiddifypanel/cache.py +3 -2
  12. hiddifypanel/celery.py +45 -0
  13. hiddifypanel/database.py +7 -0
  14. hiddifypanel/drivers/ssh_liberty_bridge_api.py +2 -1
  15. hiddifypanel/drivers/wireguard_api.py +1 -1
  16. hiddifypanel/hutils/crypto.py +27 -0
  17. hiddifypanel/hutils/flask.py +5 -3
  18. hiddifypanel/hutils/network/cf_api.py +5 -5
  19. hiddifypanel/hutils/proxy/__init__.py +1 -0
  20. hiddifypanel/hutils/proxy/clash.py +3 -3
  21. hiddifypanel/hutils/proxy/shared.py +14 -18
  22. hiddifypanel/hutils/proxy/singbox.py +4 -2
  23. hiddifypanel/hutils/proxy/wireguard.py +34 -0
  24. hiddifypanel/hutils/proxy/xray.py +3 -3
  25. hiddifypanel/hutils/proxy/xrayjson.py +10 -7
  26. hiddifypanel/models/admin.py +1 -1
  27. hiddifypanel/models/base_account.py +3 -0
  28. hiddifypanel/models/config.py +5 -2
  29. hiddifypanel/models/config_enum.py +15 -2
  30. hiddifypanel/models/proxy.py +1 -1
  31. hiddifypanel/models/user.py +2 -2
  32. hiddifypanel/panel/__init__.py +8 -8
  33. hiddifypanel/panel/admin/AdminstratorAdmin.py +16 -10
  34. hiddifypanel/panel/admin/DomainAdmin.py +132 -98
  35. hiddifypanel/panel/admin/ProxyAdmin.py +4 -0
  36. hiddifypanel/panel/admin/QuickSetup.py +48 -17
  37. hiddifypanel/panel/admin/SettingAdmin.py +6 -0
  38. hiddifypanel/panel/admin/UserAdmin.py +63 -36
  39. hiddifypanel/panel/admin/adminlte.py +1 -1
  40. hiddifypanel/panel/admin/templates/index.html +6 -4
  41. hiddifypanel/panel/admin/templates/model/user_list.html +11 -3
  42. hiddifypanel/panel/cli.py +14 -3
  43. hiddifypanel/panel/commercial/restapi/v1/tgbot.py +19 -1
  44. hiddifypanel/panel/commercial/restapi/v2/admin/system_actions.py +5 -1
  45. hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +2 -1
  46. hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +76 -6
  47. hiddifypanel/panel/common.py +5 -2
  48. hiddifypanel/panel/common_bp/login.py +14 -8
  49. hiddifypanel/panel/hlogger.py +32 -0
  50. hiddifypanel/panel/init_db.py +157 -77
  51. hiddifypanel/panel/node/__init__.py +9 -0
  52. hiddifypanel/panel/node/a.py +14 -0
  53. hiddifypanel/panel/node/hello.py +14 -0
  54. hiddifypanel/panel/node/test.proto +13 -0
  55. hiddifypanel/panel/node/test_grpc.py +40 -0
  56. hiddifypanel/panel/node/test_pb2.py +40 -0
  57. hiddifypanel/panel/node/test_pb2.pyi +17 -0
  58. hiddifypanel/panel/node/test_pb2_grpc.py +97 -0
  59. hiddifypanel/panel/usage.py +13 -3
  60. hiddifypanel/panel/user/templates/base_singbox_config.json.j2 +16 -0
  61. hiddifypanel/panel/user/templates/home/home.html +1 -2
  62. hiddifypanel/panel/user/templates/home/multi.html +1 -2
  63. hiddifypanel/panel/user/user.py +13 -19
  64. hiddifypanel/static/apps-icon/singbox.ico +0 -0
  65. hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
  66. hiddifypanel/translations/en/LC_MESSAGES/messages.po +125 -30
  67. hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
  68. hiddifypanel/translations/fa/LC_MESSAGES/messages.po +123 -32
  69. hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
  70. hiddifypanel/translations/pt/LC_MESSAGES/messages.po +114 -22
  71. hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
  72. hiddifypanel/translations/ru/LC_MESSAGES/messages.po +129 -32
  73. hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
  74. hiddifypanel/translations/zh/LC_MESSAGES/messages.po +107 -18
  75. hiddifypanel/translations.i18n/en.json +73 -14
  76. hiddifypanel/translations.i18n/fa.json +72 -13
  77. hiddifypanel/translations.i18n/fr.json +28 -10
  78. hiddifypanel/translations.i18n/my.json +1266 -0
  79. hiddifypanel/translations.i18n/pt.json +68 -9
  80. hiddifypanel/translations.i18n/ru.json +75 -16
  81. hiddifypanel/translations.i18n/zh.json +67 -8
  82. hiddifypanel-10.80.0.dist-info/METADATA +137 -0
  83. {hiddifypanel-10.70.8.dist-info → hiddifypanel-10.80.0.dist-info}/RECORD +136 -119
  84. {hiddifypanel-10.70.8.dist-info → hiddifypanel-10.80.0.dist-info}/WHEEL +1 -2
  85. hiddifypanel-10.80.0.dist-info/entry_points.txt +3 -0
  86. hiddifypanel-10.70.8.dist-info/METADATA +0 -144
  87. hiddifypanel-10.70.8.dist-info/entry_points.txt +0 -2
  88. hiddifypanel-10.70.8.dist-info/top_level.txt +0 -1
  89. {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
- model.domain = (model.domain or '').lower()
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 ipv4_list and not ipv6_list:
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
- skip_check = "*" in model.domain or model.domain == ""
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
- hutils.network.cf_api.add_or_update_dns_record(model.domain, str(ipv4_list[0]), "A", proxied=proxied)
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
- # elif model.mode==DomainType.auto_cdn_ip:
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
- dips = hutils.network.get_domain_ips(model.domain)
198
- server_ips = [*ipv4_list, *ipv6_list]
199
- if model.sub_link_only:
200
- if not dips:
201
- raise ValidationError(_("Domain can not be resolved! there is a problem in your domain")) # type: ignore
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
- if model.mode == DomainType.fake and not model.cdn_ip:
233
- model.cdn_ip = str(server_ips[0])
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
- # if model.mode==DomainType.fake and model.cdn_ip!=myip:
236
- # raise ValidationError(f"Specifying CDN IP is only valid for CDN mode")
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
- # # Update the many-to-many relationship
239
- if len(model.show_domains) == Domain.query.count():
240
- model.show_domains = []
241
- if model.mode == DomainType.old_xtls_direct:
242
- if not hconfig(ConfigEnum.xtls_enable):
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) # type: ignore
261
- msg = _("domain.reality.asn_issue") + \
262
- (f"<br> Server ASN={server_asn}<br>{d}_ASN={domain_asn}" if server_asn or domain_asn else "")
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
- for d in model.servernames.split(","):
266
- if not hutils.network.fallback_domain_compatible_with_servernames(model.domain, d):
267
- msg = _("REALITY Fallback domain is not compaitble with server names!") + f' {d} != {model.domain}'
268
- hutils.flask.flash(msg, 'warning')
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
- old_db_domain = Domain.by_domain(model.domain)
277
- if is_created or not old_db_domain or old_db_domain.mode != model.mode:
278
- # return hiddify.reinstall_action(complete_install=False, domain_changed=True)
279
- hutils.flask.flash_config_success(restart_mode=ApplyMode.apply_config, domain_changed=True)
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: get_quick_setup_form,
32
- 3: get_proxy_form}
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 = "3"
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 = "2"
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
- myip = hutils.network.get_ip(4)
224
- myip6 = hutils.network.get_ip(4)
225
- if dip and myip != dip and (not myip6 or myip6 != 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=myip, domain_ip=dip, domain=domain))
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
- myip = hutils.network.get_ip(4)
239
- if myip == dip:
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=myip, domain_ip=dip, domain=domain))
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", 'remaining_days']
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", "comment", "usage_limit", "reset_usage", "package_days", "reset_days", "mode", "uuid", "enable",]
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 / (t + 0.000001))
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 id is None or form._obj is None or form._obj.start_date is None:
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
- delattr(form, 'reset_days')
232
- delattr(form, 'reset_usage')
233
- # delattr(form,'disable_user')
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 # remaining_days(form._obj)
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
- form.usage_limit.label.text += usr_usage
245
-
246
- # if form._obj.mode==UserMode.disable:
247
- # delattr(form,'disable_user')
248
- # form.disable_user.data=form._obj.mode==UserMode.disable
249
- if form._obj.start_date:
250
- started = form._obj.start_date - datetime.date.today()
251
- msg = _("Started from %(relative)s", relative=hutils.convert.format_timedelta(started))
252
- form.package_days.label.text += f" ({msg})"
253
- if started.days <= 0:
254
- exact_start = _("Started %(days)s days ago", days=-started.days)
255
- else:
256
- exact_start = _("Will Start in %(days)s days", days=started.days)
257
- form.package_days.description += f" ({exact_start})"
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
- model.max_ips = max(3, model.max_ips or 10000)
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
- # if model.telegram_id and model.telegram_id != '0' and not re.match(r"^[1-9]\d*$", model.telegram_id):
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
- model.package_days = min(model.package_days, 10000)
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