gitscope
← marzneshin / Diff · db68a58 → db68a58
Changes db68a58..db68a58
Patch commit
diff --git a/app/db/crud.py b/app/db/crud.py
index 94b21cf..1878726 100644
--- a/app/db/crud.py
+++ b/app/db/crud.py
@@ -309,7 +309,7 @@ def create_user(db: Session, user: UserCreate, admin: Admin = None):
# proxies=proxies,
key=user.key,
services=db.query(Service)
- .filter(Service.id.in_(user.services))
+ .filter(Service.id.in_(user.service_ids))
.all(), # user.services,
status=user.status,
data_limit=(user.data_limit or None),
@@ -383,9 +383,9 @@ def update_user(db: Session, dbuser: User, modify: UserModify):
if modify.on_hold_expire_duration is not None:
dbuser.on_hold_expire_duration = modify.on_hold_expire_duration
- if modify.services is not None:
+ if modify.service_ids is not None:
dbuser.services = (
- db.query(Service).filter(Service.id.in_(modify.services)).all()
+ db.query(Service).filter(Service.id.in_(modify.service_ids)).all()
)
dbuser.edit_at = datetime.utcnow()
@@ -547,8 +547,8 @@ def get_admins(
def create_service(db: Session, service: ServiceCreate) -> Service:
dbservice = Service(
name=service.name,
- inbounds=db.query(Inbound).filter(Inbound.id.in_(service.inbounds)).all(),
- users=db.query(User).filter(User.id.in_(service.users)).all(),
+ inbounds=db.query(Inbound).filter(Inbound.id.in_(service.inbound_ids)).all(),
+ users=[],
)
db.add(dbservice)
db.commit()
@@ -568,9 +568,9 @@ def update_service(db: Session, db_service: Service, modification: ServiceModify
if modification.name is not None:
db_service.name = modification.name
- if modification.inbounds is not None:
+ if modification.inbound_ids is not None:
db_service.inbounds = (
- db.query(Inbound).filter(Inbound.id.in_(modification.inbounds)).all()
+ db.query(Inbound).filter(Inbound.id.in_(modification.inbound_ids)).all()
)
db.commit()
diff --git a/app/db/models.py b/app/db/models.py
index f322f58..255d5b8 100644
--- a/app/db/models.py
+++ b/app/db/models.py
@@ -61,12 +61,19 @@ class Service(Base):
__tablename__ = "services"
id = Column(Integer, primary_key=True)
name = Column(String(64))
- # modifications = Column(String(1024))
users = relationship("User", secondary=users_services, back_populates="services")
inbounds = relationship(
"Inbound", secondary=inbounds_services, back_populates="services"
)
+ @property
+ def inbound_ids(self):
+ return [inbound.id for inbound in self.inbounds]
+
+ @property
+ def user_ids(self):
+ return [user.id for user in self.users]
+
class User(Base):
__tablename__ = "users"
@@ -86,7 +93,6 @@ class User(Base):
".join(Inbound, Inbound.id == inbounds_services.c.inbound_id)",
viewonly=True,
distinct_target_key=True,
- lazy="joined",
)
# proxies = relationship("Proxy", back_populates="user", cascade="all, delete-orphan")
status = Column(Enum(UserStatus), nullable=False, default=UserStatus.active)
@@ -99,7 +105,6 @@ class User(Base):
"NodeUserUsage",
back_populates="user",
cascade="all,delete,delete-orphan",
- lazy="joined",
)
notification_reminders = relationship(
"NotificationReminder",
@@ -126,6 +131,10 @@ class User(Base):
on_hold_timeout = Column(DateTime)
edit_at = Column(DateTime)
+ @property
+ def service_ids(self):
+ return [service.id for service in self.services]
+
class Inbound(Base):
__tablename__ = "inbounds"
@@ -144,6 +153,10 @@ class Inbound(Base):
"InboundHost", back_populates="inbound", cascade="all, delete, delete-orphan"
)
+ @property
+ def service_ids(self):
+ return [service.id for service in self.services]
+
class InboundHost(Base):
__tablename__ = "hosts"
@@ -235,6 +248,10 @@ class Node(Base):
Float, nullable=False, server_default=text("1.0"), default=1
)
+ @property
+ def inbound_ids(self):
+ return [inbound.id for inbound in self.inbounds]
+
class NodeUserUsage(Base):
__tablename__ = "node_user_usages"
diff --git a/app/models/node.py b/app/models/node.py
index 4928d92..b10804d 100644
--- a/app/models/node.py
+++ b/app/models/node.py
@@ -1,5 +1,4 @@
from enum import Enum
-from typing import List, Optional
from pydantic import ConfigDict, BaseModel, Field
@@ -20,8 +19,8 @@ class NodeSettings(BaseModel):
certificate: str
-class NodeBase(BaseModel):
- id: Optional[int] = Field(None)
+class Node(BaseModel):
+ id: int | None = Field(None)
name: str
address: str
port: int = 53042
@@ -32,20 +31,13 @@ class NodeBase(BaseModel):
model_config = ConfigDict(from_attributes=True)
-from app.models.proxy import InboundBase
-
-
-class Node(NodeBase):
- inbounds: Optional[List[InboundBase]] = None
-
-
class NodeCreate(Node):
model_config = ConfigDict(
json_schema_extra={
"example": {
"name": "DE node",
"address": "192.168.1.1",
- "port": 62050,
+ "port": 53042,
"usage_coefficient": 1,
}
}
@@ -53,19 +45,18 @@ class NodeCreate(Node):
class NodeModify(Node):
- name: Optional[str] = Field(None)
- address: Optional[str] = Field(None)
- port: Optional[int] = Field(None)
- connection_backend: Optional[NodeConnectionBackend] = Field(None)
- status: Optional[NodeStatus] = Field(None)
- usage_coefficient: Optional[float] = Field(None, ge=0)
+ name: str | None = Field(None)
+ address: str | None = Field(None)
+ port: int | None = Field(None)
+ connection_backend: NodeConnectionBackend | None = Field(None)
+ status: NodeStatus | None = Field(None)
+ usage_coefficient: float | None = Field(None, ge=0)
model_config = ConfigDict(
json_schema_extra={
"example": {
"name": "DE node",
"address": "192.168.1.1",
- "port": 62050,
- "api_port": 62051,
+ "port": 53042,
"status": "disabled",
"usage_coefficient": 1.0,
}
@@ -74,18 +65,19 @@ class NodeModify(Node):
class NodeResponse(Node):
- xray_version: Optional[str] = None
+ xray_version: str | None = None
status: NodeStatus
- message: Optional[str] = None
+ message: str | None = None
model_config = ConfigDict(from_attributes=True)
+ inbound_ids: list[int] | None = None
class NodeUsageResponse(BaseModel):
- node_id: Optional[int] = None
+ node_id: int | None = None
node_name: str
uplink: int
downlink: int
class NodesUsageResponse(BaseModel):
- usages: List[NodeUsageResponse]
+ usages: list[NodeUsageResponse]
diff --git a/app/models/proxy.py b/app/models/proxy.py
index 08a8213..802a8a3 100644
--- a/app/models/proxy.py
+++ b/app/models/proxy.py
@@ -1,69 +1,17 @@
-import json
from enum import Enum
-from typing import Optional, List
from pydantic import ConfigDict, BaseModel, Field, field_validator
-
-class XTLSFlows(Enum):
- NONE = ""
- VISION = "xtls-rprx-vision"
-
-
-class ShadowsocksMethods(Enum):
- AES_128_GCM = "aes-128-gcm"
- AES_256_GCM = "aes-256-gcm"
- CHACHA20_POLY1305 = "chacha20-ietf-poly1305"
+from app.models.node import Node
class ProxyTypes(str, Enum):
# proxy_type = protocol
-
VMess = "vmess"
VLESS = "vless"
Trojan = "trojan"
Shadowsocks = "shadowsocks"
- @property
- def settings_model(self):
- if self == self.VMess:
- return VMessSettings
- if self == self.VLESS:
- return VLESSSettings
- if self == self.Trojan:
- return TrojanSettings
- if self == self.Shadowsocks:
- return ShadowsocksSettings
-
-
-class ProxySettings(BaseModel):
- @classmethod
- def from_dict(cls, proxy_type: ProxyTypes, _dict: dict):
- return ProxyTypes(proxy_type).settings_model.parse_obj(_dict)
-
- def dict(self, *, no_obj=False, **kwargs):
- if no_obj:
- return json.loads(self.json())
- return super().dict(**kwargs)
-
-
-class VMessSettings(ProxySettings):
- id: str = Field(nullable=False)
-
-
-class VLESSSettings(ProxySettings):
- id: str = Field(nullable=False)
- flow: XTLSFlows = XTLSFlows.NONE
-
-
-class TrojanSettings(ProxySettings):
- password: str = Field(nullable=False)
-
-
-class ShadowsocksSettings(ProxySettings):
- password: str = Field(nullable=False)
- method: ShadowsocksMethods = ShadowsocksMethods.CHACHA20_POLY1305
-
class InboundHostSecurity(str, Enum):
inbound_default = "inbound_default"
@@ -114,17 +62,17 @@ class FragmentSettings(BaseModel):
class InboundHost(BaseModel):
remark: str
address: str
- port: Optional[int] = Field(None)
- sni: Optional[str] = Field(None)
- host: Optional[str] = Field(None)
- path: Optional[str] = Field(None)
+ port: int | None = Field(None)
+ sni: str | None = Field(None)
+ host: str | None = Field(None)
+ path: str | None = Field(None)
security: InboundHostSecurity = InboundHostSecurity.inbound_default
alpn: InboundHostALPN = InboundHostALPN.none
fingerprint: InboundHostFingerprint = InboundHostFingerprint.none
- allowinsecure: Optional[bool] = None
- is_disabled: Optional[bool] = None
+ allowinsecure: bool | None = None
+ is_disabled: bool | None = None
mux: bool = Field(False)
- fragment: Optional[FragmentSettings] = Field(None)
+ fragment: FragmentSettings | None = Field(None)
model_config = ConfigDict(from_attributes=True)
@field_validator("remark", "address", "path")
@@ -142,19 +90,11 @@ class InboundHostResponse(InboundHost):
id: int
-class InboundBase(BaseModel):
- id: Optional[int] = Field(None)
- tag: str = Field()
- protocol: ProxyTypes = Field()
+class Inbound(BaseModel):
+ id: int
+ tag: str
+ protocol: ProxyTypes
config: str
- node_id: Optional[int] = Field(None)
+ node: Node
+ service_ids: list[int]
model_config = ConfigDict(from_attributes=True)
-
-
-from app.models.service import ServiceBase
-from app.models.node import NodeBase
-
-
-class Inbound(InboundBase):
- services: Optional[List[ServiceBase]] = None
- node: Optional[NodeBase] = None
diff --git a/app/models/service.py b/app/models/service.py
index 9bae7f0..5a8f119 100644
--- a/app/models/service.py
+++ b/app/models/service.py
@@ -1,57 +1,35 @@
-from typing import List, Optional
+from pydantic import ConfigDict, BaseModel, Field
-from pydantic import ConfigDict, BaseModel, Field, computed_field
-
-class ServiceBase(BaseModel):
- id: Optional[int] = None
- name: Optional[str] = Field(None)
+class Service(BaseModel):
+ id: int | None = None
+ name: str | None = Field(None)
model_config = ConfigDict(from_attributes=True)
-from app.models.proxy import InboundBase
-from app.models.user import UserBase
-
-
-class Service(ServiceBase):
- users: List[UserBase] = Field([])
- inbounds: List[InboundBase] = Field([])
-
-
-class ServiceCreate(ServiceBase):
- users: List[int] = Field([])
- inbounds: List[int] = Field([])
+class ServiceCreate(Service):
+ inbound_ids: list[int] = Field([])
model_config = ConfigDict(
json_schema_extra={
"example": {
"name": "my service 1",
- "inbounds": [1, 5, 9],
- "users": [1, 2, 3],
+ "inbound_ids": [1, 5, 9],
}
}
)
-class ServiceModify(ServiceBase):
- inbounds: Optional[List[int]] = Field(None)
+class ServiceModify(Service):
+ inbound_ids: list[int] | None = Field(None)
model_config = ConfigDict(
json_schema_extra={
- "example": {"id": 2, "name": "my service 2", "inbounds": [1, 2, 3]}
+ "example": {"id": 2, "name": "my service 2", "inbound_ids": [1, 2, 3]}
}
)
class ServiceResponse(Service):
- id: int
-
- @computed_field
- @property
- def user_ids(self) -> List[int]:
- return [u.id for u in self.users]
-
- @computed_field
- @property
- def inbound_ids(self) -> List[int]:
- return [i.id for i in self.inbounds]
+ inbound_ids: list[int]
+ user_ids: list[int]
model_config = ConfigDict(from_attributes=True)
diff --git a/app/models/user.py b/app/models/user.py
index 3f6ef82..478d416 100644
--- a/app/models/user.py
+++ b/app/models/user.py
@@ -1,7 +1,7 @@
import secrets
from datetime import datetime
from enum import Enum
-from typing import List, Optional, Union, Annotated, Literal
+from typing import List, Union, Annotated, Literal
from pydantic import (
field_validator,
@@ -48,39 +48,31 @@ class UserDataLimitResetStrategy(str, Enum):
year = "year"
-class UserBase(BaseModel):
- id: Optional[int] = None
+class User(BaseModel):
+ id: int | None = None
username: Annotated[str, StringConstraints(to_lower=True, pattern=USERNAME_REGEXP)]
expire: Union[datetime, None, Literal[0]] = Field(None)
key: str = Field(default_factory=lambda: secrets.token_hex(16))
- data_limit: Optional[int] = Field(
+ data_limit: int | None = Field(
ge=0, default=None, description="data_limit can be 0 or greater"
)
enabled: bool = Field(default=True)
data_limit_reset_strategy: UserDataLimitResetStrategy = (
UserDataLimitResetStrategy.no_reset
)
- note: Optional[Annotated[str, Field(max_length=500)]] = None
- sub_updated_at: Optional[datetime] = Field(None)
- sub_last_user_agent: Optional[str] = Field(None)
- online_at: Optional[datetime] = Field(None)
- on_hold_expire_duration: Optional[int] = Field(None)
- on_hold_timeout: Optional[datetime] = Field(None)
+ note: Annotated[str, Field(max_length=500)] | None = None
+ sub_updated_at: datetime | None = Field(None)
+ sub_last_user_agent: str | None = Field(None)
+ online_at: datetime | None = Field(None)
+ on_hold_expire_duration: int | None = Field(None)
+ on_hold_timeout: datetime | None = Field(None)
model_config = ConfigDict(from_attributes=True)
-from app.models.service import ServiceBase
-from app.models.proxy import InboundBase
-
-
-class User(UserBase):
- pass
-
-
class UserCreate(User):
status: UserStatusCreate = Field(UserStatusCreate.active)
- services: List[int] = Field([])
+ service_ids: list[int] = Field([])
model_config = ConfigDict(
json_schema_extra={
"example": {
@@ -125,9 +117,9 @@ class UserCreate(User):
class UserModify(User):
- status: Optional[UserStatusModify] = Field(None)
- services: List[int] = Field([])
- data_limit_reset_strategy: Optional[UserDataLimitResetStrategy] = Field(None)
+ status: UserStatusModify | None = Field(None)
+ service_ids: list[int] | None = Field(None)
+ data_limit_reset_strategy: UserDataLimitResetStrategy | None = Field(None)
model_config = ConfigDict(
json_schema_extra={
"example": {
@@ -170,18 +162,10 @@ class UserResponse(User):
used_traffic: int
lifetime_used_traffic: int = 0
created_at: datetime
- inbounds: List[InboundBase] = Field([])
- services: List[ServiceBase] = Field([])
- links: List[str] | None = [] # Field(None)
+ service_ids: list[int]
- # subscription_url: str = ""
model_config = ConfigDict(from_attributes=True)
- @computed_field
- @property
- def service_ids(self) -> List[int]:
- return [s.id for s in self.services]
-
@computed_field
@property
def subscription_url(self) -> str:
diff --git a/app/routes/service.py b/app/routes/service.py
index 60f3dfe..7f977a8 100644
--- a/app/routes/service.py
+++ b/app/routes/service.py
@@ -6,9 +6,11 @@ from fastapi_pagination.links import Page
from app import marznode
from app.db import crud
-from app.db.models import Service
+from app.db.models import Service, User
from app.dependencies import DBDep, sudo_admin
+from app.models.proxy import Inbound
from app.models.service import ServiceCreate, ServiceModify, ServiceResponse
+from app.models.user import UserResponse
router = APIRouter(
prefix="/services", dependencies=[Depends(sudo_admin)], tags=["Service"]
@@ -52,6 +54,36 @@ def get_service(id: int, db: DBDep):
return dbservice
+@router.get("/{id}/users", response_model=Page[UserResponse])
+def get_service_users(id: int, db: DBDep):
+ """
+ Get service users
+ """
+ service = crud.get_service(db, id)
+
+ if not service:
+ raise HTTPException(status_code=404, detail="Service not found")
+
+ query = db.query(User).join(User.services).where(Service.id == service.id)
+
+ return paginate(query)
+
+
+@router.get("/{id}/inbounds", response_model=Page[Inbound])
+def get_service_inbounds(id: int, db: DBDep):
+ """
+ Get service inbounds
+ """
+ service = crud.get_service(db, id)
+
+ if not service:
+ raise HTTPException(status_code=404, detail="Service not found")
+
+ query = db.query(Inbound).join(Inbound.services).where(Service.id == service.id)
+
+ return paginate(query)
+
+
@router.put("/{id}", response_model=ServiceResponse)
async def modify_service(id: int, modification: ServiceModify, db: DBDep):
"""
diff --git a/app/routes/subscription.py b/app/routes/subscription.py
index bf08877..75e5ea6 100644
--- a/app/routes/subscription.py
+++ b/app/routes/subscription.py
@@ -43,7 +43,7 @@ def user_subscription(
user: UserResponse = UserResponse.model_validate(db_user)
if "text/html" in accept_header:
- links = generate_subscription(user=user, config_format="v2ray").split()
+ links = generate_subscription(user=db_user, config_format="v2ray").split()
return HTMLResponse(
render_template(SUBSCRIPTION_PAGE_TEMPLATE, {"user": user, "links": links})
)
@@ -64,18 +64,20 @@ def user_subscription(
crud.update_user_sub(db, db_user, user_agent)
if re.match("^([Cc]lash-verge|[Cc]lash-?[Mm]eta)", user_agent):
- conf = generate_subscription(user=user, config_format="clash-meta")
+ conf = generate_subscription(user=db_user, config_format="clash-meta")
return Response(content=conf, media_type="text/yaml", headers=response_headers)
elif re.match("^([Cc]lash|[Ss]tash)", user_agent):
- conf = generate_subscription(user=user, config_format="clash")
+ conf = generate_subscription(user=db_user, config_format="clash")
return Response(content=conf, media_type="text/yaml", headers=response_headers)
elif re.match("^(SFA|SFI|SFM|SFT)", user_agent):
- conf = generate_subscription(user=user, config_format="sing-box")
+ conf = generate_subscription(user=db_user, config_format="sing-box")
return Response(
content=conf, media_type="application/json", headers=response_headers
)
else:
- conf = generate_subscription(user=user, config_format="v2ray", as_base64=True)
+ conf = generate_subscription(
+ user=db_user, config_format="v2ray", as_base64=True
+ )
return Response(content=conf, media_type="text/plain", headers=response_headers)
@@ -119,24 +121,26 @@ def user_subscription_with_client_type(
}
if client_type == "clash-meta":
- conf = generate_subscription(user=user, config_format="clash-meta")
+ conf = generate_subscription(user=db_user, config_format="clash-meta")
return Response(content=conf, media_type="text/yaml", headers=response_headers)
elif client_type == "sing-box":
- conf = generate_subscription(user=user, config_format="sing-box")
+ conf = generate_subscription(user=db_user, config_format="sing-box")
return Response(
content=conf, media_type="application/json", headers=response_headers
)
elif client_type == "clash":
- conf = generate_subscription(user=user, config_format="clash")
+ conf = generate_subscription(user=db_user, config_format="clash")
return Response(content=conf, media_type="text/yaml", headers=response_headers)
elif client_type == "v2ray":
- conf = generate_subscription(user=user, config_format="v2ray", as_base64=True)
+ conf = generate_subscription(
+ user=db_user, config_format="v2ray", as_base64=True
+ )
return Response(content=conf, media_type="text/plain", headers=response_headers)
elif client_type == "xray":
return Response(
- content=generate_subscription(user=user, config_format="xray"),
+ content=generate_subscription(user=db_user, config_format="xray"),
headers=response_headers,
)
else:
diff --git a/app/routes/user.py b/app/routes/user.py
index c6894ba..b160b8c 100644
--- a/app/routes/user.py
+++ b/app/routes/user.py
@@ -11,6 +11,7 @@ from fastapi_pagination.links import Page
from app import marznode
from app.db import crud, User
+from app.db.models import Service
from app.dependencies import (
DBDep,
AdminDep,
@@ -19,6 +20,7 @@ from app.dependencies import (
StartDateDep,
EndDateDep,
)
+from app.models.service import ServiceResponse
from app.models.user import (
UserCreate,
UserModify,
@@ -89,7 +91,6 @@ async def add_user(new_user: UserCreate, db: DBDep, admin: AdminDep):
- **data_limit** must be in Bytes, e.g. 1073741824B = 1GB
- **services** list of service ids
"""
- # TODO expire should be datetime instead of timestamp
try:
db_user = crud.create_user(
@@ -225,6 +226,21 @@ async def remove_user(db_user: UserDep, db: DBDep, admin: AdminDep):
return {}
+@router.get("/{username}/services", response_model=Page[ServiceResponse])
+def get_user_services(username: str, db: DBDep):
+ """
+ Get user services
+ """
+ user = crud.get_user(db, username)
+
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ query = db.query(Service).join(Service.users).where(User.username == username)
+
+ return paginate(query)
+
+
@router.post("/{username}/reset", response_model=UserResponse)
async def reset_user_data_usage(db_user: UserDep, db: DBDep, admin: AdminDep):
"""
diff --git a/app/utils/share.py b/app/utils/share.py
index af8fc9a..f1e2e61 100644
--- a/app/utils/share.py
+++ b/app/utils/share.py
@@ -5,7 +5,7 @@ import secrets
from collections import defaultdict
from datetime import datetime as dt, timedelta
from importlib import resources
-from typing import TYPE_CHECKING, Literal, Union, List
+from typing import Literal, Union, List
from uuid import UUID
from jdatetime import date as jd
@@ -16,6 +16,7 @@ from app.db import GetDB, crud
from app.models.proxy import (
InboundHostSecurity,
)
+from app.models.user import UserResponse
from app.models.user import UserStatus
from app.utils.keygen import gen_uuid, gen_password
from app.utils.system import get_public_ip, readable_size
@@ -25,9 +26,6 @@ from config import (
CLASH_SUBSCRIPTION_TEMPLATE,
)
-if TYPE_CHECKING:
- from app.models.user import UserResponse
-
SERVER_IP = get_public_ip()
STATUS_EMOJIS = {
@@ -65,7 +63,9 @@ def generate_subscription(
inbounds = user.inbounds
key = user.key
- extra_data = user.model_dump(exclude={"subscription_url", "services", "inbounds"})
+ extra_data = UserResponse.model_validate(user).model_dump(
+ exclude={"subscription_url", "services", "inbounds"}
+ )
format_variables = setup_format_variables(extra_data)
if config_format not in subscription_handlers.keys():
diff --git a/dashboard/src/components/entity-field-card/index.tsx b/dashboard/src/components/entity-field-card/index.tsx
new file mode 100644
index 0000000..f6755a2
--- /dev/null
+++ b/dashboard/src/components/entity-field-card/index.tsx
@@ -0,0 +1,34 @@
+import { LucideIcon } from 'lucide-react';
+import { Label, Separator } from '@marzneshin/components';
+import { FC } from 'react'
+
+interface EntityFieldCardProps {
+ FirstIcon: LucideIcon;
+ SecondIcon: LucideIcon;
+ firstAmount: number | string;
+ secondAmount: number | string;
+ name: string;
+}
+
+
+export const EntityCardProperty = ({ Icon, amount }: { Icon: LucideIcon, amount: number | string }) => (
+ <div className="flex flex-row items-center w-full">
+ <Icon className="w-4 h-4 font-light mr-1" />
+ {amount}
+ </div>
+)
+
+export const EntityFieldCard: FC<EntityFieldCardProps> = (
+ { FirstIcon, SecondIcon, firstAmount, secondAmount, name }
+) => {
+ return (
+ <div className="w-full flex justify-between flex-row items-center">
+ <Label>{name}</Label>
+ <div className="flex flex-row items-center gap-1">
+ <EntityCardProperty Icon={FirstIcon} amount={firstAmount} />
+ <Separator className="w-4 rotate-[-75deg]" />
+ <EntityCardProperty Icon={SecondIcon} amount={secondAmount} />
+ </div>
+ </div>
+ )
+};
diff --git a/dashboard/src/components/index.ts b/dashboard/src/components/index.ts
index 5356af5..89c04dd 100644
--- a/dashboard/src/components/index.ts
+++ b/dashboard/src/components/index.ts
@@ -12,3 +12,4 @@ export * from "./stacks";
export * from "./widgets";
export * from "./charts";
export * from "./support-us";
+export * from "./entity-field-card";
diff --git a/dashboard/src/features/services/dialogs/mutation/fields/inbounds.tsx b/dashboard/src/features/services/dialogs/mutation/fields/inbounds.tsx
index 510b489..f0ed4d3 100644
--- a/dashboard/src/features/services/dialogs/mutation/fields/inbounds.tsx
+++ b/dashboard/src/features/services/dialogs/mutation/fields/inbounds.tsx
@@ -5,7 +5,7 @@ import {
FormLabel,
FormField,
Checkbox,
- Badge,
+ EntityFieldCard,
} from "@marzneshin/components";
import {
useInboundsQuery,
@@ -25,7 +25,7 @@ export const InboundCard = ({
field,
}: {
inbound: InboundType;
- field: ControllerRenderProps<FieldValues, "inbounds">;
+ field: ControllerRenderProps<FieldValues, "inbound_ids">;
}) => {
return (
<div
@@ -47,17 +47,13 @@ export const InboundCard = ({
}}
/>
<FormLabel className="flex flex-row justify-between items-center w-full">
- {inbound.tag}
- <div className="flex gap-2">
- <Badge className="py-1 px-2">
- {" "}
- <GlobeLock className="mr-1 w-4" /> {inbound.protocol}{" "}
- </Badge>
- <Badge className="py-1 px-2">
- {" "}
- <Box className="mr-1 w-4" /> {inbound.node.name}{" "}
- </Badge>
- </div>
+ <EntityFieldCard
+ FirstIcon={GlobeLock}
+ SecondIcon={Box}
+ firstAmount={inbound.protocol}
+ secondAmount={inbound.node.name}
+ name={inbound.tag}
+ />
</FormLabel>
</div>
);
@@ -71,7 +67,7 @@ export const InboundsField = () => {
return (
<FormField
control={form.control}
- name="inbounds"
+ name="inbound_ids"
render={() => (
<FormItem>
<FormLabel>{t("inbounds")}</FormLabel>
@@ -79,7 +75,7 @@ export const InboundsField = () => {
<FormField
key={inbound.id}
control={form.control}
- name="inbounds"
+ name="inbound_ids"
render={({ field }) => (
<FormItem key={inbound.id}>
<FormControl>
diff --git a/dashboard/src/features/services/dialogs/mutation/index.tsx b/dashboard/src/features/services/dialogs/mutation/index.tsx
index cfdadd3..910bea4 100644
--- a/dashboard/src/features/services/dialogs/mutation/index.tsx
+++ b/dashboard/src/features/services/dialogs/mutation/index.tsx
@@ -19,11 +19,12 @@ import { NameField, InboundsField } from "./fields";
export const ServiceSchema = z.object({
id: z.number().optional(),
- inbounds: z.array(z.number()).refine((value) => value.some((item) => item), {
+ inbound_ids: z.array(z.number()).refine((value) => value.some((item) => item), {
message: "You have to select at least one inbound.",
}),
name: z.string().trim().min(1),
});
+
type ServiceCreateType = z.infer<typeof ServiceSchema>;
interface MutationDialogProps {
@@ -34,7 +35,7 @@ interface MutationDialogProps {
const getDefaultValues = (): ServiceCreateType => ({
name: "",
- inbounds: [],
+ inbound_ids: [],
});
export const MutationDialog: FC<MutationDialogProps> = ({
diff --git a/dashboard/src/features/services/services/service-inbounds.query.ts b/dashboard/src/features/services/services/service-inbounds.query.ts
new file mode 100644
index 0000000..9fed833
--- /dev/null
+++ b/dashboard/src/features/services/services/service-inbounds.query.ts
@@ -0,0 +1,38 @@
+import { useQuery } from "@tanstack/react-query";
+import { fetch } from "@marzneshin/utils";
+import { InboundType } from "@marzneshin/features/inbounds";
+
+interface FetchServiceInboundsType {
+ queryKey: [string, number, number, number]
+}
+
+interface UseServiceInboundsQueryProps {
+ serviceId: number;
+ page?: number;
+ size?: number;
+}
+
+export async function fetchServiceInbounds({
+ queryKey
+}: FetchServiceInboundsType): Promise<InboundType[]> {
+ return fetch(`/services/${queryKey[1]}/inbounds`, {
+ query: {
+ page: queryKey[2],
+ size: queryKey[3]
+ }
+ }).then((result) => {
+ return result.items;
+ });
+}
+
+export const ServicesQueryFetchKey = "services";
+
+export const useInboundsServiceQuery = ({
+ serviceId, page = 1, size = 50
+}: UseServiceInboundsQueryProps) => {
+ return useQuery({
+ queryKey: [ServicesQueryFetchKey, serviceId, page, size],
+ queryFn: fetchServiceInbounds,
+ initialData: []
+ })
+}
diff --git a/dashboard/src/features/services/services/service-users.query.ts b/dashboard/src/features/services/services/service-users.query.ts
index 256645c..9b3a3c1 100644
--- a/dashboard/src/features/services/services/service-users.query.ts
+++ b/dashboard/src/features/services/services/service-users.query.ts
@@ -1,20 +1,36 @@
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
-import { ServiceType } from "../types";
import { UserType } from "@marzneshin/features/users";
-export async function fetchUsersService({ queryKey }: { queryKey: [string, number] }): Promise<UserType[]> {
- return fetch(`/services/${queryKey[1]}`).then((result) => {
- return result.users;
+interface FetchServiceUsersType {
+ queryKey: [string, number, number, number]
+}
+
+interface UseServiceUsersQueryProps {
+ serviceId: number;
+ page?: number;
+ size?: number;
+}
+
+export async function fetchServiceUsers({ queryKey }: FetchServiceUsersType): Promise<UserType[]> {
+ return fetch(`/services/${queryKey[1]}/users`, {
+ query: {
+ page: queryKey[2],
+ size: queryKey[3]
+ }
+ }).then((result) => {
+ return result.items;
});
}
export const ServicesQueryFetchKey = "services";
-export const useUsersServiceQuery = (service: ServiceType) => {
+export const useUsersServiceQuery = ({
+ serviceId, page = 1, size = 50
+}: UseServiceUsersQueryProps) => {
return useQuery({
- queryKey: [ServicesQueryFetchKey, service.id],
- queryFn: fetchUsersService,
+ queryKey: [ServicesQueryFetchKey, serviceId, page, size],
+ queryFn: fetchServiceUsers,
initialData: []
})
}
diff --git a/dashboard/src/features/services/services/services.query.ts b/dashboard/src/features/services/services/services.query.ts
index aa0cfc2..1736bd0 100644
--- a/dashboard/src/features/services/services/services.query.ts
+++ b/dashboard/src/features/services/services/services.query.ts
@@ -7,23 +7,14 @@ import {
} from "@marzneshin/components";
import { useQuery } from "@tanstack/react-query";
-interface ServiceResponse {
- id: number
- name: string
- users: any[]
- inbounds: any[]
- user_ids: number[]
- inbound_ids: number[]
-}
-
export async function fetchServices({ queryKey }: EntityQueryKeyType): FetchEntityReturn<ServiceType> {
- return fetch(`/services?page=${queryKey[1]}&size=${queryKey[2]}&name=${queryKey[3]}`).then((result) => {
- const services: ServiceType[] = result.items.map((fetchedService: ServiceResponse) => {
- const service: ServiceType = fetchedService;
- service.users = fetchedService.user_ids
- service.inbounds = fetchedService.inbound_ids
- return service
- })
+ return fetch(`/services`, {
+ query: {
+ page: queryKey[1],
+ size: queryKey[2]
+ }
+ }).then((result) => {
+ const services: ServiceType[] = result.items;
return {
entity: services,
pageCount: result.pages
@@ -33,9 +24,9 @@ export async function fetchServices({ queryKey }: EntityQueryKeyType): FetchEnti
export const ServicesQueryFetchKey = "services";
-export const useServicesQuery = ({ page, size, search }: UseEntityQueryProps) => {
+export const useServicesQuery = ({ page, size, search = ""}: UseEntityQueryProps) => {
return useQuery({
- queryKey: [ServicesQueryFetchKey, page, size, search ? search : ""],
+ queryKey: [ServicesQueryFetchKey, page, size, search],
queryFn: fetchServices,
initialData: { entity: [], pageCount: 0 }
})
diff --git a/dashboard/src/features/services/tables/inbounds/index.tsx b/dashboard/src/features/services/tables/inbounds/index.tsx
index 0605c8e..5d7cebb 100644
--- a/dashboard/src/features/services/tables/inbounds/index.tsx
+++ b/dashboard/src/features/services/tables/inbounds/index.tsx
@@ -23,7 +23,7 @@ export const ServiceInboundsTable: FC<ServiceInboundsTableProps> = ({
useEffect(() => {
setSelectedInbound((prevSelected) => {
const updatedSelected: RowSelectionState = { ...prevSelected };
- for (const inboundId of service.inbounds) {
+ for (const inboundId of service.inbound_ids) {
for (const [i, fetchedInbound] of data.entries()) {
if (fetchedInbound.id === inboundId) {
updatedSelected[i] = true;
@@ -32,13 +32,13 @@ export const ServiceInboundsTable: FC<ServiceInboundsTableProps> = ({
}
return updatedSelected;
});
- }, [data, service.inbounds]);
+ }, [data, service.inbound_ids]);
const handleApply = useCallback(() => {
const selectedInboundIds = Object.keys(selectedInbound)
.filter((key) => selectedInbound[Number.parseInt(key)])
.map((key) => data[Number.parseInt(key)].id);
- updateService({ ...service, inbounds: selectedInboundIds });
+ updateService({ ...service, inbound_ids: selectedInboundIds });
}, [data, selectedInbound, service, updateService]);
const disabled = Object.keys(selectedInbound).length < 1;
diff --git a/dashboard/src/features/services/tables/services/columns.tsx b/dashboard/src/features/services/tables/services/columns.tsx
index ce19109..f8b92f4 100644
--- a/dashboard/src/features/services/tables/services/columns.tsx
+++ b/dashboard/src/features/services/tables/services/columns.tsx
@@ -19,12 +19,12 @@ export const columns = (actions: ColumnAction): ColumnDef<ServiceType>[] => ([
{
accessorKey: "users",
header: ({ column }) => <DataTableColumnHeader title={i18n.t('users')} column={column} />,
- cell: ({ row }) => `${row.original.users.length}`
+ cell: ({ row }) => `${row.original.user_ids.length}`
},
{
accessorKey: "inbounds",
header: ({ column }) => <DataTableColumnHeader title={i18n.t('inbounds')} column={column} />,
- cell: ({ row }) => `${row.original.inbounds.length}`
+ cell: ({ row }) => `${row.original.inbound_ids.length}`
},
{
id: "actions",
diff --git a/dashboard/src/features/services/tables/users/index.tsx b/dashboard/src/features/services/tables/users/index.tsx
index 76eda2a..4fb842f 100644
--- a/dashboard/src/features/services/tables/users/index.tsx
+++ b/dashboard/src/features/services/tables/users/index.tsx
@@ -10,7 +10,7 @@ interface ServicesUsersTableProps {
}
export const ServicesUsersTable: FC<ServicesUsersTableProps> = ({ service }) => {
- const { data } = useUsersServiceQuery(service);
+ const { data } = useUsersServiceQuery({ serviceId: service.id });
return (
<div>
diff --git a/dashboard/src/features/services/types/services-mutation.type.ts b/dashboard/src/features/services/types/services-mutation.type.ts
index d6decb1..c3990ed 100644
--- a/dashboard/src/features/services/types/services-mutation.type.ts
+++ b/dashboard/src/features/services/types/services-mutation.type.ts
@@ -1,7 +1,7 @@
import { z } from "zod";
export const ServiceCreateSchema = z.object({
- inbounds: z.array(z.number()),
+ inbound_ids: z.array(z.number()),
name: z.string().trim().min(1),
id: z.number().optional()
})
diff --git a/dashboard/src/features/services/types/services.type.ts b/dashboard/src/features/services/types/services.type.ts
index 42c6877..4175f57 100644
--- a/dashboard/src/features/services/types/services.type.ts
+++ b/dashboard/src/features/services/types/services.type.ts
@@ -2,6 +2,6 @@
export interface ServiceType {
id: number;
name: string;
- users: number[];
- inbounds: number[];
+ user_ids: number[];
+ inbound_ids: number[];
}
diff --git a/dashboard/src/features/users/components/services-card.tsx b/dashboard/src/features/users/components/services-card.tsx
index e941cc3..093b655 100644
--- a/dashboard/src/features/users/components/services-card.tsx
+++ b/dashboard/src/features/users/components/services-card.tsx
@@ -1,33 +1,25 @@
-
-import { Label, Separator } from '@marzneshin/components';
+import { EntityFieldCard } from '@marzneshin/components';
import { ServiceType } from '@marzneshin/features/services';
-import { LucideIcon, ServerIcon, UserIcon } from 'lucide-react';
+import { ServerIcon, UserIcon } from 'lucide-react';
import { FC } from 'react'
interface ServiceCardProps {
service: ServiceType
}
-const ServiceProperty = ({ Icon, amount }: { Icon: LucideIcon, amount: number }) => (
- <div className="flex flex-row items-center w-full">
- <Icon className="w-4 h-4 font-light" />
- {amount}
- </div>
-)
export const ServiceCard: FC<ServiceCardProps> = (
{ service }
) => {
- if (service.users && service.inbounds) {
+ if (service.user_ids && service.inbound_ids) {
return (
- <div className="w-full flex justify-between flex-row items-center">
- <Label>{service.name}</Label>
- <div className="flex flex-row items-center gap-1">
- <ServiceProperty Icon={UserIcon} amount={service.users.length} />
- <Separator className="w-4 rotate-[-75deg]" />
- <ServiceProperty Icon={ServerIcon} amount={service.inbounds.length} />
- </div>
- </div>
+ <EntityFieldCard
+ FirstIcon={UserIcon}
+ SecondIcon={ServerIcon}
+ firstAmount={service.user_ids.length}
+ secondAmount={service.inbound_ids.length}
+ name={service.name}
+ />
)
}
};
diff --git a/dashboard/src/features/users/dialogs/mutation/fields/services.tsx b/dashboard/src/features/users/dialogs/mutation/fields/services.tsx
index 0fcd47b..e51443b 100644
--- a/dashboard/src/features/users/dialogs/mutation/fields/services.tsx
+++ b/dashboard/src/features/users/dialogs/mutation/fields/services.tsx
@@ -23,7 +23,7 @@ import { useTranslation } from "react-i18next";
interface ServiceCheckboxCardProps {
service: ServiceType;
- field: ControllerRenderProps<FieldValues, "services">;
+ field: ControllerRenderProps<FieldValues, "service_ids">;
}
const ServiceCheckboxCard: FC<ServiceCheckboxCardProps> = ({
@@ -62,7 +62,7 @@ export const ServicesField: FC = () => {
return (
<FormField
control={form.control}
- name="services"
+ name="service_ids"
render={() => (
<FormItem>
<FormLabel>{t("services")}</FormLabel>
@@ -71,7 +71,7 @@ export const ServicesField: FC = () => {
<FormField
key={service.id}
control={form.control}
- name="services"
+ name="service_ids"
render={({ field }) => (
<FormItem key={service.id}>
<FormControl>
diff --git a/dashboard/src/features/users/dialogs/mutation/index.tsx b/dashboard/src/features/users/dialogs/mutation/index.tsx
index 4a3b23f..4fd0669 100644
--- a/dashboard/src/features/users/dialogs/mutation/index.tsx
+++ b/dashboard/src/features/users/dialogs/mutation/index.tsx
@@ -37,7 +37,7 @@ export const UsersMutationDialog: FC<UsersMutationDialogProps> = ({
const { t } = useTranslation();
const getDefaultValues = useCallback(
(): UserMutationType => ({
- services: [],
+ service_ids: [],
data_limit: undefined,
expire: null,
username: "",
diff --git a/dashboard/src/features/users/dialogs/mutation/schema.ts b/dashboard/src/features/users/dialogs/mutation/schema.ts
index 507c910..223f5a9 100644
--- a/dashboard/src/features/users/dialogs/mutation/schema.ts
+++ b/dashboard/src/features/users/dialogs/mutation/schema.ts
@@ -19,6 +19,6 @@ export const UserSchema = z.object({
status: z.string(),
on_hold_expire_duration: z.number().nullable().optional(),
on_hold_timeout: z.string().nullable().optional(),
- services: z.array(z.number().or(z.string())).nonempty({ message: 'At least one service is required' })
+ service_ids: z.array(z.number().or(z.string())).nonempty({ message: 'At least one service is required' })
.transform((v) => v.map(Number)),
});
diff --git a/dashboard/src/features/users/services/users.query.ts b/dashboard/src/features/users/services/users.query.ts
index bcb86b8..3ac702b 100644
--- a/dashboard/src/features/users/services/users.query.ts
+++ b/dashboard/src/features/users/services/users.query.ts
@@ -10,11 +10,6 @@ import {
export type SortUserBy = "username" | "used_traffic" | "data_limit" | "expire" | "created_at"
-interface UserResponse extends UserType {
- service_ids: number[]
- services: any[]
-}
-
export async function fetchUsers({ queryKey }: EntityQueryKeyType): FetchEntityReturn<UserType> {
return fetch(`/users`, {
query: {
@@ -25,13 +20,8 @@ export async function fetchUsers({ queryKey }: EntityQueryKeyType): FetchEntityR
order_by: queryKey[4]
}
}).then((result) => {
- const users: UserType[] = result.items.map((fetchedUser: UserResponse) => {
- const user: UserType = fetchedUser;
- user.services = fetchedUser.service_ids
- return user
- })
return {
- entity: users,
+ entity: result.items,
pageCount: result.pages
};
});
diff --git a/dashboard/src/features/users/tables/services/columns.tsx b/dashboard/src/features/users/tables/services/columns.tsx
index 93253a3..4c0dc5e 100644
--- a/dashboard/src/features/users/tables/services/columns.tsx
+++ b/dashboard/src/features/users/tables/services/columns.tsx
@@ -35,12 +35,12 @@ export const columns: ColumnDef<ServiceType>[] = [
},
{
accessorKey: 'users',
- header: ({ column }) => <DataTableColumnHeader title={i18n.t('Users')} column={column} />,
- cell: ({ row }) => row.original.users.length
+ header: ({ column }) => <DataTableColumnHeader title={i18n.t('users')} column={column} />,
+ cell: ({ row }) => row.original.user_ids.length
},
{
accessorKey: 'inbounds',
header: ({ column }) => <DataTableColumnHeader title={i18n.t('inbounds')} column={column} />,
- cell: ({ row }) => row.original.inbounds.length
+ cell: ({ row }) => row.original.inbound_ids.length
}
];
diff --git a/dashboard/src/features/users/tables/services/index.tsx b/dashboard/src/features/users/tables/services/index.tsx
index edf614d..d75bb1f 100644
--- a/dashboard/src/features/users/tables/services/index.tsx
+++ b/dashboard/src/features/users/tables/services/index.tsx
@@ -26,7 +26,7 @@ export const UserServicesTable: FC<UserServicesTableProps> = ({ user }) => {
useEffect(() => {
setSelectedService((prevSelected) => {
const updatedSelected: RowSelectionState = { ...prevSelected };
- for (const serviceId of user.services) {
+ for (const serviceId of user.service_ids) {
for (const [i, fetchedService] of data.entity.entries()) {
if (fetchedService.id === serviceId) {
updatedSelected[i] = true;
@@ -35,13 +35,13 @@ export const UserServicesTable: FC<UserServicesTableProps> = ({ user }) => {
}
return updatedSelected;
});
- }, [data, user.services]);
+ }, [data, user.service_ids]);
const handleApply = useCallback(() => {
const selectedServiceIds = Object.keys(selectedService)
.filter((key) => selectedService[Number.parseInt(key)])
.map((key) => data.entity[Number.parseInt(key)].id);
- updateUser({ ...user, services: selectedServiceIds });
+ updateUser({ ...user, service_ids: selectedServiceIds });
}, [data, selectedService, user, updateUser]);
const disabled = Object.keys(selectedService).length < 1;
diff --git a/dashboard/src/features/users/types/index.ts b/dashboard/src/features/users/types/index.ts
index 0309c57..6229374 100644
--- a/dashboard/src/features/users/types/index.ts
+++ b/dashboard/src/features/users/types/index.ts
@@ -30,14 +30,14 @@ export interface UserType {
status: Status;
links: string[];
subscription_url: string;
- services: number[];
+ service_ids: number[];
note: string;
online_at: string;
}
export type UserMutationType = Pick<
UserType,
- | 'services'
+ | 'service_ids'
| 'expire'
| 'data_limit'
| 'data_limit_reset_strategy'