Changes log:
feat: add user URI API endpoint feat: integrate show_user_uri_api in users page refactor: remove URI generation from viewmodel
This commit is contained in:
@ -1,4 +1,4 @@
|
|||||||
from pydantic import BaseModel
|
from typing import Optional
|
||||||
from pydantic import BaseModel, RootModel
|
from pydantic import BaseModel, RootModel
|
||||||
|
|
||||||
|
|
||||||
@ -37,3 +37,9 @@ class EditUserInputBody(BaseModel):
|
|||||||
renew_password: bool = False
|
renew_password: bool = False
|
||||||
renew_creation_date: bool = False
|
renew_creation_date: bool = False
|
||||||
blocked: bool = False
|
blocked: bool = False
|
||||||
|
|
||||||
|
class UserUriResponse(BaseModel):
|
||||||
|
username: str
|
||||||
|
ipv4: str | None = None
|
||||||
|
ipv6: str | None = None
|
||||||
|
normal_sub: str | None = None
|
||||||
@ -150,11 +150,33 @@ async def reset_user_api(username: str):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
|
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
|
||||||
|
|
||||||
# TODO implement show user uri endpoint
|
@router.get('/{username}/uri', response_model=UserUriResponse)
|
||||||
# @router.get('/{username}/uri', response_model=TODO)
|
async def show_user_uri_api(username: str):
|
||||||
# async def show_user_uri(username: str):
|
"""
|
||||||
# try:
|
Get the URI information for a user in JSON format.
|
||||||
# res = cli_api.show_user_uri(username)
|
|
||||||
# return res
|
Args:
|
||||||
# except Exception as e:
|
username: The username of the user.
|
||||||
# raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
|
|
||||||
|
Returns:
|
||||||
|
UserUriResponse: An object containing URI information for the user.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: 404 if the user is not found, 400 if another error occurs.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
uri_data_list = cli_api.show_user_uri_json([username])
|
||||||
|
if not uri_data_list:
|
||||||
|
raise HTTPException(status_code=404, detail=f'URI for user {username} not found.')
|
||||||
|
uri_data = uri_data_list[0]
|
||||||
|
if uri_data.get('error'):
|
||||||
|
raise HTTPException(status_code=404, detail=f"{uri_data['error']}")
|
||||||
|
return uri_data
|
||||||
|
except cli_api.ScriptNotFoundError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f'Server script error: {str(e)}')
|
||||||
|
except cli_api.CommandExecutionError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f'Error executing script: {str(e)}')
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f'Unexpected error: {str(e)}')
|
||||||
|
|||||||
@ -4,57 +4,6 @@ from datetime import datetime, timedelta
|
|||||||
import cli_api
|
import cli_api
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
|
||||||
type: str
|
|
||||||
link: str
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_username(username: str) -> list['Config']:
|
|
||||||
raw_uri = Config.__get_user_configs_uri(username)
|
|
||||||
if not raw_uri:
|
|
||||||
return []
|
|
||||||
|
|
||||||
res = []
|
|
||||||
for line in raw_uri.splitlines():
|
|
||||||
config = Config.__parse_user_configs_uri_line(line)
|
|
||||||
if config:
|
|
||||||
res.append(config)
|
|
||||||
return res
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __get_user_configs_uri(username: str) -> str:
|
|
||||||
# This command is equivalent to `show-user-uri --username $username --ipv 4 --all --singbox --normalsub`
|
|
||||||
raw_uri = cli_api.show_user_uri(username, False, 4, True, True, True)
|
|
||||||
|
|
||||||
return raw_uri.strip() if raw_uri else ''
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __parse_user_configs_uri_line(line: str) -> "Config | None":
|
|
||||||
config_type = ''
|
|
||||||
config_link = ''
|
|
||||||
|
|
||||||
line = line.strip()
|
|
||||||
if line.startswith("hy2://"):
|
|
||||||
if "@" in line:
|
|
||||||
ip_version = "IPv6" if line.split("@")[1].count(":") > 1 else "IPv4"
|
|
||||||
config_type = ip_version
|
|
||||||
config_link = line
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
elif line.startswith("https://"):
|
|
||||||
if "singbox" in line.lower():
|
|
||||||
config_type = "Singbox"
|
|
||||||
elif "normal" in line.lower():
|
|
||||||
config_type = "Normal-SUB"
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
config_link = line
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return Config(type=config_type, link=config_link)
|
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
status: str
|
status: str
|
||||||
@ -63,7 +12,6 @@ class User(BaseModel):
|
|||||||
expiry_date: datetime
|
expiry_date: datetime
|
||||||
expiry_days: int
|
expiry_days: int
|
||||||
enable: bool
|
enable: bool
|
||||||
configs: list[Config]
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(username: str, user_data: dict):
|
def from_dict(username: str, user_data: dict):
|
||||||
@ -107,7 +55,6 @@ class User(BaseModel):
|
|||||||
'expiry_date': expiry_date,
|
'expiry_date': expiry_date,
|
||||||
'expiry_days': expiration_days,
|
'expiry_days': expiration_days,
|
||||||
'enable': False if user_data.get('blocked', False) else True,
|
'enable': False if user_data.get('blocked', False) else True,
|
||||||
'configs': Config.from_username(user_data['username'])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -114,18 +114,6 @@
|
|||||||
data-username="{{ user.username }}">
|
data-username="{{ user.username }}">
|
||||||
<i class="fas fa-qrcode"></i>
|
<i class="fas fa-qrcode"></i>
|
||||||
</a>
|
</a>
|
||||||
<div id="userConfigs-{{ user.username }}" style="display: none;">
|
|
||||||
{% for config in user.configs %}
|
|
||||||
<div class="config-container" data-link="{{ config.link }}">
|
|
||||||
<span class="config-type">{{ config.type }}:</span>
|
|
||||||
{% if config.type == "Singbox" or config.type == "Normal-SUB" %}
|
|
||||||
<span class="config-link-text">{{ config.link }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="config-link-text">{{ config.link }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
<button type="button" class="btn btn-sm btn-info edit-user"
|
<button type="button" class="btn btn-sm btn-info edit-user"
|
||||||
@ -652,86 +640,103 @@
|
|||||||
|
|
||||||
// QR Code Modal
|
// QR Code Modal
|
||||||
$("#qrcodeModal").on("show.bs.modal", function (event) {
|
$("#qrcodeModal").on("show.bs.modal", function (event) {
|
||||||
const button = $(event.relatedTarget);
|
const button = $(event.relatedTarget);
|
||||||
const configContainer = $(`#userConfigs-${button.data("username")}`);
|
const username = button.data("username");
|
||||||
const qrcodesContainer = $("#qrcodesContainer");
|
const qrcodesContainer = $("#qrcodesContainer");
|
||||||
qrcodesContainer.empty();
|
qrcodesContainer.empty();
|
||||||
|
|
||||||
configContainer.find(".config-container").each(function () {
|
const userUriApiUrl = "{{ url_for('show_user_uri_api', username='USERNAME_PLACEHOLDER') }}";
|
||||||
const configLink = $(this).data("link");
|
const url = userUriApiUrl.replace("USERNAME_PLACEHOLDER", encodeURIComponent(username));
|
||||||
const configType = $(this).find(".config-type").text().replace(":", "");
|
|
||||||
|
|
||||||
let displayType = configType;
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method: "GET",
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (response) {
|
||||||
|
// console.log("API Response:", response);
|
||||||
|
|
||||||
const hashMatch = configLink.match(/#(.+)$/);
|
const configs = [
|
||||||
if (hashMatch && hashMatch[1]) {
|
{ type: "IPv4", link: response.ipv4 },
|
||||||
const hashValue = hashMatch[1];
|
{ type: "IPv6", link: response.ipv6 },
|
||||||
if (hashValue.includes("IPv4") || hashValue.includes("IPv6")) {
|
{ type: "Normal-SUB", link: response.normal_sub }
|
||||||
displayType = hashValue;
|
];
|
||||||
}
|
|
||||||
} else if (configLink.includes("ipv4") || configLink.includes("IPv4")) {
|
|
||||||
displayType = "IPv4";
|
configs.forEach(config => {
|
||||||
} else if (configLink.includes("ipv6") || configLink.includes("IPv6")) {
|
if (config.link) {
|
||||||
displayType = "IPv6";
|
const displayType = config.type;
|
||||||
|
const configLink = config.link;
|
||||||
|
const qrCodeId = `qrcode-${displayType}-${Math.random().toString(36).substring(2, 10)}`;
|
||||||
|
|
||||||
|
const card = $(`
|
||||||
|
<div class="card d-inline-block my-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="${qrCodeId}" class="mx-auto cursor-pointer"></div>
|
||||||
|
<br>
|
||||||
|
<div class="config-type-text mt-2 text-center">${displayType}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
qrcodesContainer.append(card);
|
||||||
|
|
||||||
|
const qrCodeStyling = new QRCodeStyling({
|
||||||
|
width: 180,
|
||||||
|
height: 180,
|
||||||
|
data: configLink,
|
||||||
|
margin: 5,
|
||||||
|
dotsOptions: {
|
||||||
|
color: "#212121",
|
||||||
|
type: "square"
|
||||||
|
},
|
||||||
|
cornersSquareOptions: {
|
||||||
|
color: "#212121",
|
||||||
|
type: "square"
|
||||||
|
},
|
||||||
|
backgroundOptions: {
|
||||||
|
color: "#FAFAFA",
|
||||||
|
},
|
||||||
|
imageOptions: {
|
||||||
|
hideBackgroundDots: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
qrCodeStyling.append(document.getElementById(qrCodeId));
|
||||||
|
|
||||||
|
card.on("click", function () {
|
||||||
|
navigator.clipboard.writeText(configLink)
|
||||||
|
.then(() => {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: displayType + " link copied!",
|
||||||
|
showConfirmButton: false,
|
||||||
|
timer: 1500,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Failed to copy link: ", err);
|
||||||
|
Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Failed to copy link",
|
||||||
|
text: "Please copy manually.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function (error) {
|
||||||
|
console.error("Error fetching user URI:", error);
|
||||||
|
Swal.fire({
|
||||||
|
title: "Error!",
|
||||||
|
text: "Failed to fetch user configuration URIs.",
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const qrCodeId = `qrcode-${displayType}-${Math.random().toString(36).substring(2, 10)}`;
|
|
||||||
|
|
||||||
const card = $(`
|
|
||||||
<div class="card d-inline-block my-2">
|
|
||||||
<div class="card-body">
|
|
||||||
<div id="${qrCodeId}" class="mx-auto cursor-pointer"></div>
|
|
||||||
<br>
|
|
||||||
<div class="config-type-text mt-2 text-center">${displayType}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
|
|
||||||
qrcodesContainer.append(card);
|
|
||||||
|
|
||||||
const qrCodeStyling = new QRCodeStyling({
|
|
||||||
width: 180,
|
|
||||||
height: 180,
|
|
||||||
data: configLink,
|
|
||||||
margin: 5,
|
|
||||||
dotsOptions: {
|
|
||||||
color: "#212121",
|
|
||||||
type: "square"
|
|
||||||
},
|
|
||||||
cornersSquareOptions: {
|
|
||||||
color: "#212121",
|
|
||||||
type: "square"
|
|
||||||
},
|
|
||||||
backgroundOptions: {
|
|
||||||
color: "#FAFAFA",
|
|
||||||
},
|
|
||||||
imageOptions: {
|
|
||||||
hideBackgroundDots: true,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
qrCodeStyling.append(document.getElementById(qrCodeId));
|
|
||||||
|
|
||||||
card.on("click", function () {
|
|
||||||
navigator.clipboard.writeText(configLink)
|
|
||||||
.then(() => {
|
|
||||||
Swal.fire({
|
|
||||||
icon: "success",
|
|
||||||
title: displayType + " link copied!",
|
|
||||||
showConfirmButton: false,
|
|
||||||
timer: 1500,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Failed to copy link: ", err);
|
|
||||||
Swal.fire({
|
|
||||||
icon: "error",
|
|
||||||
title: "Failed to copy link",
|
|
||||||
text: "Please copy manually.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#qrcodeModal .modal-content").on("click", function (e) {
|
$("#qrcodeModal .modal-content").on("click", function (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user