diff --git a/app/db/crud.py b/app/db/crud.py
@@ -102,6 +102,7 @@ def get_node_users(
query = (
db.query(User.id, User.username, User.key, Inbound)
.distinct()
+ .where(User.enabled is True)
.join(Inbound.services)
.join(Service.users)
.filter(Inbound.node_id == node_id)
diff --git a/app/db/migrations/versions/20faa9f18c0a_init.py b/app/db/migrations/versions/20faa9f18c0a_init.py
@@ -12,7 +12,7 @@ import sqlalchemy as sa
from app.utils.crypto import generate_certificate
# revision identifiers, used by Alembic.
-revision = '20faa9f18c0a'
+revision = "20faa9f18c0a"
down_revision = None
branch_labels = None
depends_on = None
@@ -20,192 +20,313 @@ depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
- op.create_table('admins',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('username', sa.String(length=32), nullable=True),
- sa.Column('hashed_password', sa.String(length=128), nullable=True),
- sa.Column('created_at', sa.DateTime(), nullable=True),
- sa.Column('is_sudo', sa.Boolean(), nullable=True),
- sa.Column('password_reset_at', sa.DateTime(), nullable=True),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_index(op.f('ix_admins_username'), 'admins', ['username'], unique=True)
-
- jwttable = op.create_table('jwt',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('secret_key', sa.String(length=64), nullable=False),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "admins",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("username", sa.String(length=32), nullable=True),
+ sa.Column("hashed_password", sa.String(length=128), nullable=True),
+ sa.Column("created_at", sa.DateTime(), nullable=True),
+ sa.Column("is_sudo", sa.Boolean(), nullable=True),
+ sa.Column("password_reset_at", sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_index(op.f("ix_admins_username"), "admins", ["username"], unique=True)
+
+ jwttable = op.create_table(
+ "jwt",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("secret_key", sa.String(length=64), nullable=False),
+ sa.PrimaryKeyConstraint("id"),
)
op.bulk_insert(jwttable, [{"id": 1, "secret_key": os.urandom(32).hex()}])
- nodestable = op.create_table('nodes',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('name', sa.String(length=256), nullable=True),
- sa.Column('address', sa.String(length=256), nullable=True),
- sa.Column('port', sa.Integer(), nullable=True),
- sa.Column('connection_backend', sa.String(), nullable=True),
- sa.Column('xray_version', sa.String(length=32), nullable=True),
- sa.Column('status', sa.Enum('healthy', 'unhealthy', 'disabled', name='nodestatus'), nullable=False),
- sa.Column('last_status_change', sa.DateTime(), nullable=True),
- sa.Column('message', sa.String(length=1024), nullable=True),
- sa.Column('created_at', sa.DateTime(), nullable=True),
- sa.Column('uplink', sa.BigInteger(), nullable=True),
- sa.Column('downlink', sa.BigInteger(), nullable=True),
- sa.Column('usage_coefficient', sa.Float(), server_default=sa.text('(1.0)'), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('name'),
- sa.UniqueConstraint('address', 'port')
- )
- op.bulk_insert(nodestable, [{"name": "local", "address": "127.0.0.1", "port": 53042, "connection_backend": "grpcio", "status": "unhealthy"}])
- op.create_table('services',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('name', sa.String(length=64), nullable=True),
- sa.PrimaryKeyConstraint('id')
+ nodestable = op.create_table(
+ "nodes",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("name", sa.String(length=256), nullable=True),
+ sa.Column("address", sa.String(length=256), nullable=True),
+ sa.Column("port", sa.Integer(), nullable=True),
+ sa.Column("connection_backend", sa.String(), nullable=True),
+ sa.Column("xray_version", sa.String(length=32), nullable=True),
+ sa.Column(
+ "status",
+ sa.Enum("healthy", "unhealthy", "disabled", name="nodestatus"),
+ nullable=False,
+ ),
+ sa.Column("last_status_change", sa.DateTime(), nullable=True),
+ sa.Column("message", sa.String(length=1024), nullable=True),
+ sa.Column("created_at", sa.DateTime(), nullable=True),
+ sa.Column("uplink", sa.BigInteger(), nullable=True),
+ sa.Column("downlink", sa.BigInteger(), nullable=True),
+ sa.Column(
+ "usage_coefficient",
+ sa.Float(),
+ server_default=sa.text("(1.0)"),
+ nullable=False,
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ sa.UniqueConstraint("name"),
+ sa.UniqueConstraint("address", "port"),
+ )
+ op.bulk_insert(
+ nodestable,
+ [
+ {
+ "name": "local",
+ "address": "127.0.0.1",
+ "port": 53042,
+ "connection_backend": "grpcio",
+ "status": "unhealthy",
+ }
+ ],
+ )
+ op.create_table(
+ "services",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("name", sa.String(length=64), nullable=True),
+ sa.PrimaryKeyConstraint("id"),
)
- systable = op.create_table('system',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('uplink', sa.BigInteger(), nullable=True),
- sa.Column('downlink', sa.BigInteger(), nullable=True),
- sa.PrimaryKeyConstraint('id')
+ systable = op.create_table(
+ "system",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("uplink", sa.BigInteger(), nullable=True),
+ sa.Column("downlink", sa.BigInteger(), nullable=True),
+ sa.PrimaryKeyConstraint("id"),
)
op.bulk_insert(systable, [{"id": 1, "uplink": 0, "downlink": 0}])
- tlstable = op.create_table('tls',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('key', sa.String(length=4096), nullable=False),
- sa.Column('certificate', sa.String(length=2048), nullable=False),
- sa.PrimaryKeyConstraint('id')
+ tlstable = op.create_table(
+ "tls",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("key", sa.String(length=4096), nullable=False),
+ sa.Column("certificate", sa.String(length=2048), nullable=False),
+ sa.PrimaryKeyConstraint("id"),
)
tls = generate_certificate()
- op.bulk_insert(tlstable, [{"id": 1, "key": tls['key'], "certificate": tls['cert']}])
+ op.bulk_insert(tlstable, [{"id": 1, "key": tls["key"], "certificate": tls["cert"]}])
- op.create_table('inbounds',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('protocol', sa.Enum('VMess', 'VLESS', 'Trojan', 'Shadowsocks', name='proxytypes'), nullable=True),
- sa.Column('tag', sa.String(length=256), nullable=False),
- sa.Column('config', sa.String(), nullable=False),
- sa.Column('node_id', sa.Integer(), nullable=True),
- sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint("node_id", "tag")
- )
- op.create_index(op.f('ix_inbounds_node_id'), 'inbounds', ['node_id'], unique=False)
- op.create_table('node_usages',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('created_at', sa.DateTime(), nullable=False),
- sa.Column('node_id', sa.Integer(), nullable=True),
- sa.Column('uplink', sa.BigInteger(), nullable=True),
- sa.Column('downlink', sa.BigInteger(), nullable=True),
- sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('created_at', 'node_id')
- )
- op.create_table('users',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('username', sa.String(length=32), nullable=True),
- sa.Column('key', sa.String(length=64), nullable=True),
- sa.Column('status', sa.Enum('active', 'disabled', 'limited', 'expired', 'on_hold', name='userstatus'), nullable=False),
- sa.Column('used_traffic', sa.BigInteger(), nullable=True),
- sa.Column('data_limit', sa.BigInteger(), nullable=True),
- sa.Column('data_limit_reset_strategy', sa.Enum('no_reset', 'day', 'week', 'month', 'year', name='userdatalimitresetstrategy'), nullable=False),
- sa.Column('ip_limit', sa.Integer(), nullable=False),
- sa.Column('settings', sa.String(), nullable=True),
- sa.Column('expire', sa.DateTime(), nullable=True),
- sa.Column('admin_id', sa.Integer(), nullable=True),
- sa.Column('sub_updated_at', sa.DateTime(), nullable=True),
- sa.Column('sub_last_user_agent', sa.String(length=512), nullable=True),
- sa.Column('created_at', sa.DateTime(), nullable=True),
- sa.Column('note', sa.String(length=500), nullable=True),
- sa.Column('online_at', sa.DateTime(), nullable=True),
- sa.Column('on_hold_expire_duration', sa.BigInteger(), nullable=True),
- sa.Column('on_hold_timeout', sa.DateTime(), nullable=True),
- sa.Column('edit_at', sa.DateTime(), nullable=True),
- sa.ForeignKeyConstraint(['admin_id'], ['admins.id'], ),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('key')
- )
- op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
- op.create_table('hosts',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('remark', sa.String(length=256), nullable=False),
- sa.Column('address', sa.String(length=256), nullable=False),
- sa.Column('port', sa.Integer(), nullable=True),
- sa.Column('path', sa.String(length=256), nullable=True),
- sa.Column('sni', sa.String(length=256), nullable=True),
- sa.Column('host', sa.String(length=256), nullable=True),
- sa.Column('security', sa.Enum('inbound_default', 'none', 'tls', name='inboundhostsecurity'), nullable=False),
- sa.Column('alpn', sa.Enum('none', 'h2', 'http/1.1', 'h2,http/1.1', name='proxyhostalpn'), server_default='none', nullable=False),
- sa.Column('fingerprint', sa.Enum('none', 'chrome', 'firefox', 'safari', 'ios', 'android', 'edge', '360', 'qq', 'random', 'randomized', name='proxyhostfingerprint'), server_default='none', nullable=False),
- sa.Column('inbound_id', sa.Integer(), nullable=False),
- sa.Column('allowinsecure', sa.Boolean(), nullable=True),
- sa.Column('is_disabled', sa.Boolean(), nullable=True),
- sa.ForeignKeyConstraint(['inbound_id'], ['inbounds.id'], ),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('inbounds_services',
- sa.Column('inbound_id', sa.Integer(), nullable=False),
- sa.Column('service_id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['inbound_id'], ['inbounds.id'], ),
- sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
- sa.PrimaryKeyConstraint('inbound_id', 'service_id')
- )
- op.create_table('node_user_usages',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('created_at', sa.DateTime(), nullable=False),
- sa.Column('user_id', sa.Integer(), nullable=True),
- sa.Column('node_id', sa.Integer(), nullable=True),
- sa.Column('used_traffic', sa.BigInteger(), nullable=True),
- sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
- sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('created_at', 'user_id', 'node_id')
- )
- op.create_table('notification_reminders',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('user_id', sa.Integer(), nullable=True),
- sa.Column('type', sa.Enum('expiration_date', 'data_usage', name='remindertype'), nullable=False),
- sa.Column('expires_at', sa.DateTime(), nullable=True),
- sa.Column('created_at', sa.DateTime(), nullable=True),
- sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('user_usage_logs',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('user_id', sa.Integer(), nullable=True),
- sa.Column('used_traffic_at_reset', sa.BigInteger(), nullable=False),
- sa.Column('reset_at', sa.DateTime(), nullable=True),
- sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('users_services',
- sa.Column('user_id', sa.Integer(), nullable=False),
- sa.Column('service_id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
- sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
- sa.PrimaryKeyConstraint('user_id', 'service_id')
+ op.create_table(
+ "inbounds",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column(
+ "protocol",
+ sa.Enum("VMess", "VLESS", "Trojan", "Shadowsocks", name="proxytypes"),
+ nullable=True,
+ ),
+ sa.Column("tag", sa.String(length=256), nullable=False),
+ sa.Column("config", sa.String(), nullable=False),
+ sa.Column("node_id", sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(
+ ["node_id"],
+ ["nodes.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ sa.UniqueConstraint("node_id", "tag"),
+ )
+ op.create_index(op.f("ix_inbounds_node_id"), "inbounds", ["node_id"], unique=False)
+ op.create_table(
+ "node_usages",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("created_at", sa.DateTime(), nullable=False),
+ sa.Column("node_id", sa.Integer(), nullable=True),
+ sa.Column("uplink", sa.BigInteger(), nullable=True),
+ sa.Column("downlink", sa.BigInteger(), nullable=True),
+ sa.ForeignKeyConstraint(
+ ["node_id"],
+ ["nodes.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ sa.UniqueConstraint("created_at", "node_id"),
+ )
+ op.create_table(
+ "users",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("username", sa.String(length=32), nullable=True),
+ sa.Column("key", sa.String(length=64), nullable=True),
+ sa.Column(
+ "status",
+ sa.Enum("active", "limited", "expired", "on_hold", name="userstatus"),
+ nullable=False,
+ ),
+ sa.Column("used_traffic", sa.BigInteger(), nullable=True),
+ sa.Column("data_limit", sa.BigInteger(), nullable=True),
+ sa.Column(
+ "data_limit_reset_strategy",
+ sa.Enum(
+ "no_reset",
+ "day",
+ "week",
+ "month",
+ "year",
+ name="userdatalimitresetstrategy",
+ ),
+ nullable=False,
+ ),
+ sa.Column("ip_limit", sa.Integer(), nullable=False),
+ sa.Column("settings", sa.String(), nullable=True),
+ sa.Column("expire", sa.DateTime(), nullable=True),
+ sa.Column("admin_id", sa.Integer(), nullable=True),
+ sa.Column("sub_updated_at", sa.DateTime(), nullable=True),
+ sa.Column("sub_last_user_agent", sa.String(length=512), nullable=True),
+ sa.Column("created_at", sa.DateTime(), nullable=True),
+ sa.Column("note", sa.String(length=500), nullable=True),
+ sa.Column("online_at", sa.DateTime(), nullable=True),
+ sa.Column("on_hold_expire_duration", sa.BigInteger(), nullable=True),
+ sa.Column("on_hold_timeout", sa.DateTime(), nullable=True),
+ sa.Column("edit_at", sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(
+ ["admin_id"],
+ ["admins.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ sa.UniqueConstraint("key"),
+ )
+ op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True)
+ op.create_table(
+ "hosts",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("remark", sa.String(length=256), nullable=False),
+ sa.Column("address", sa.String(length=256), nullable=False),
+ sa.Column("port", sa.Integer(), nullable=True),
+ sa.Column("path", sa.String(length=256), nullable=True),
+ sa.Column("sni", sa.String(length=256), nullable=True),
+ sa.Column("host", sa.String(length=256), nullable=True),
+ sa.Column(
+ "security",
+ sa.Enum("inbound_default", "none", "tls", name="inboundhostsecurity"),
+ nullable=False,
+ ),
+ sa.Column(
+ "alpn",
+ sa.Enum("none", "h2", "http/1.1", "h2,http/1.1", name="proxyhostalpn"),
+ server_default="none",
+ nullable=False,
+ ),
+ sa.Column(
+ "fingerprint",
+ sa.Enum(
+ "none",
+ "chrome",
+ "firefox",
+ "safari",
+ "ios",
+ "android",
+ "edge",
+ "360",
+ "qq",
+ "random",
+ "randomized",
+ name="proxyhostfingerprint",
+ ),
+ server_default="none",
+ nullable=False,
+ ),
+ sa.Column("inbound_id", sa.Integer(), nullable=False),
+ sa.Column("allowinsecure", sa.Boolean(), nullable=True),
+ sa.Column("is_disabled", sa.Boolean(), nullable=True),
+ sa.ForeignKeyConstraint(
+ ["inbound_id"],
+ ["inbounds.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_table(
+ "inbounds_services",
+ sa.Column("inbound_id", sa.Integer(), nullable=False),
+ sa.Column("service_id", sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(
+ ["inbound_id"],
+ ["inbounds.id"],
+ ),
+ sa.ForeignKeyConstraint(
+ ["service_id"],
+ ["services.id"],
+ ),
+ sa.PrimaryKeyConstraint("inbound_id", "service_id"),
+ )
+ op.create_table(
+ "node_user_usages",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("created_at", sa.DateTime(), nullable=False),
+ sa.Column("user_id", sa.Integer(), nullable=True),
+ sa.Column("node_id", sa.Integer(), nullable=True),
+ sa.Column("used_traffic", sa.BigInteger(), nullable=True),
+ sa.ForeignKeyConstraint(
+ ["node_id"],
+ ["nodes.id"],
+ ),
+ sa.ForeignKeyConstraint(
+ ["user_id"],
+ ["users.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ sa.UniqueConstraint("created_at", "user_id", "node_id"),
+ )
+ op.create_table(
+ "notification_reminders",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("user_id", sa.Integer(), nullable=True),
+ sa.Column(
+ "type",
+ sa.Enum("expiration_date", "data_usage", name="remindertype"),
+ nullable=False,
+ ),
+ sa.Column("expires_at", sa.DateTime(), nullable=True),
+ sa.Column("created_at", sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(
+ ["user_id"],
+ ["users.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_table(
+ "user_usage_logs",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("user_id", sa.Integer(), nullable=True),
+ sa.Column("used_traffic_at_reset", sa.BigInteger(), nullable=False),
+ sa.Column("reset_at", sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(
+ ["user_id"],
+ ["users.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_table(
+ "users_services",
+ sa.Column("user_id", sa.Integer(), nullable=False),
+ sa.Column("service_id", sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(
+ ["service_id"],
+ ["services.id"],
+ ),
+ sa.ForeignKeyConstraint(
+ ["user_id"],
+ ["users.id"],
+ ),
+ sa.PrimaryKeyConstraint("user_id", "service_id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('users_services')
- op.drop_table('user_usage_logs')
- op.drop_table('notification_reminders')
- op.drop_table('node_user_usages')
- op.drop_table('inbounds_services')
- op.drop_table('hosts')
- op.drop_index(op.f('ix_users_username'), table_name='users')
- op.drop_table('users')
- op.drop_table('node_usages')
- op.drop_index(op.f('ix_inbounds_node_id'), table_name='inbounds')
- op.drop_table('inbounds')
- op.drop_table('tls')
- op.drop_table('system')
- op.drop_table('services')
- op.drop_table('nodes')
- op.drop_table('jwt')
- op.drop_index(op.f('ix_admins_username'), table_name='admins')
- op.drop_table('admins')
+ op.drop_table("users_services")
+ op.drop_table("user_usage_logs")
+ op.drop_table("notification_reminders")
+ op.drop_table("node_user_usages")
+ op.drop_table("inbounds_services")
+ op.drop_table("hosts")
+ op.drop_index(op.f("ix_users_username"), table_name="users")
+ op.drop_table("users")
+ op.drop_table("node_usages")
+ op.drop_index(op.f("ix_inbounds_node_id"), table_name="inbounds")
+ op.drop_table("inbounds")
+ op.drop_table("tls")
+ op.drop_table("system")
+ op.drop_table("services")
+ op.drop_table("nodes")
+ op.drop_table("jwt")
+ op.drop_index(op.f("ix_admins_username"), table_name="admins")
+ op.drop_table("admins")
# ### end Alembic commands ###
diff --git a/app/db/migrations/versions/e8325f600a49_user_enable.py b/app/db/migrations/versions/e8325f600a49_user_enable.py
@@ -0,0 +1,34 @@
+"""Add enabled to users
+
+Revision ID: e8325f600a49
+Revises: 20faa9f18c0a
+Create Date: 2024-05-01 13:39:53.944195
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "e8325f600a49"
+down_revision = "20faa9f18c0a"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column(
+ "users",
+ sa.Column(
+ "enabled", sa.Boolean(), server_default=sa.sql.true(), nullable=False
+ ),
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column("users", "enabled")
+ # ### end Alembic commands ###
diff --git a/app/db/models.py b/app/db/models.py
@@ -1,6 +1,7 @@
import os
from datetime import datetime
+import sqlalchemy.sql
from sqlalchemy import (
BigInteger,
Boolean,
@@ -72,6 +73,9 @@ class User(Base):
id = Column(Integer, primary_key=True)
username = Column(String(32), unique=True, index=True)
key = Column(String(64), unique=True)
+ enabled = Column(
+ Boolean, nullable=False, default=True, server_default=sqlalchemy.sql.true()
+ )
services = relationship(
"Service", secondary=users_services, back_populates="users", lazy="joined"
)
diff --git a/app/models/user.py b/app/models/user.py
@@ -25,15 +25,13 @@ class ReminderType(str, Enum):
class UserStatus(str, Enum):
active = "active"
- disabled = "disabled"
+ on_hold = "on_hold"
limited = "limited"
expired = "expired"
- on_hold = "on_hold"
class UserStatusModify(str, Enum):
active = "active"
- disabled = "disabled"
on_hold = "on_hold"
@@ -58,6 +56,7 @@ class UserBase(BaseModel):
data_limit: Optional[int] = 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
)
diff --git a/app/routes/user.py b/app/routes/user.py
@@ -175,11 +175,6 @@ async def modify_user(
status_change = bool(
modifications.status and modifications.status != db_user.status
)
- if status_change and (
- modifications.status == UserStatus.disabled
- and old_status in {UserStatus.active, UserStatus.on_hold}
- ):
- await marznode.operations.remove_user(db_user)
old_inbounds = {(i.node_id, i.protocol, i.tag) for i in db_user.inbounds}
new_user = crud.update_user(db, db_user, modifications)
@@ -187,7 +182,11 @@ async def modify_user(
user = UserResponse.model_validate(db_user)
inbound_change = old_inbounds != new_inbounds
- if inbound_change and user.status in {UserStatus.active, UserStatus.on_hold}:
+ if (
+ inbound_change
+ and user.enabled
+ and user.status in {UserStatus.active, UserStatus.on_hold}
+ ):
await marznode.operations.update_user(new_user, old_inbounds)
asyncio.create_task(report.user_updated(user=user, by=admin))
@@ -245,6 +244,44 @@ async def reset_user_data_usage(db_user: UserDep, db: DBDep, admin: AdminDep):
return user
+@router.post("/{username}/enable", response_model=UserResponse)
+async def enable_user(db_user: UserDep, db: DBDep, admin: AdminDep):
+ """
+ Enables a user
+ """
+ if db_user.enabled:
+ raise HTTPException(409, "User is already enabled")
+ db_user.enabled = True
+ db.commit()
+
+ await marznode.operations.update_user(db_user)
+
+ user = UserResponse.model_validate(db_user)
+
+ logger.info("User `%s` has been enabled", db_user.username)
+
+ return user
+
+
+@router.post("/{username}/disable", response_model=UserResponse)
+async def disable_user(db_user: UserDep, db: DBDep, admin: AdminDep):
+ """
+ Disables a user
+ """
+ if not db_user.enabled:
+ raise HTTPException(409, "User is not enabled")
+ db_user.enabled = False
+ db.commit()
+
+ await marznode.operations.remove_user(db_user)
+
+ user = UserResponse.model_validate(db_user)
+
+ logger.info("User `%s` has been disabled", db_user.username)
+
+ return user
+
+
@router.post("/{username}/revoke_sub", response_model=UserResponse)
async def revoke_user_subscription(db_user: UserDep, db: DBDep, admin: AdminDep):
"""
diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json
@@ -35,6 +35,8 @@
"subscription": "Subscription",
"subscription_link": "Subscription Link",
"fingerprint": "Fingerprint",
+ "enable": "Enable",
+ "disable": "Disable",
"page": {
"settings": {
"certificate": {
@@ -199,6 +201,13 @@
"title": "{{name}} data usage is successfuly reseted",
"desc": ""
}
+ },
+ "user_status": {
+ "error": "Failed to toggle {{name}} enable status",
+ "success": {
+ "title": "{{name}} enable status is successfuly updated",
+ "desc": ""
+ }
}
},
"delete-confirmation": {
diff --git a/dashboard/public/locales/kur.json b/dashboard/public/locales/kur.json
@@ -23,11 +23,13 @@
"path": "Path",
"host": "Host",
"alpn": "ALPN",
- "security": "Security",
- "user_info": "Users Info",
- "subscription": "Subscription",
- "subscription_link": "Subscription Link",
+ "security": "Asayiş",
+ "user_info": "Zanyarî bekarhêneran",
+ "subscription": "Beşdarî",
+ "subscription_link": "Link Beşdarî",
"fingerprint": "Fingerprint",
+ "enable": "Çalakî ke",
+ "disable": "Naçalakî ke",
"page": {
"settings": {
"certificate": {
@@ -192,6 +194,13 @@
"title": "{{name}} data usage is successfuly reseted",
"desc": ""
}
+ },
+ "user_status": {
+ "error": "Şikistî hêna le gorrînî doxî çalak kirdinî {{name}}",
+ "success": {
+ "title": "Doxî çalakî {{name}} nwêkirawe",
+ "desc": "Doxî çalak kirdin be serkewtûyî nwêkirawe"
+ }
}
},
"delete-confirmation": {
diff --git a/dashboard/src/features/users/components/users-status.tsx b/dashboard/src/features/users/components/users-status.tsx
@@ -1,7 +1,17 @@
import { FC } from 'react';
import { Badge, Label } from '@marzneshin/components';
import { StatusType } from '@marzneshin/types';
-import { AlarmClock, BrickWall, CalendarX, Cloud, PowerCircle, PowerOff, Radio, ServerCrash, Zap, ZapOff } from 'lucide-react';
+import {
+ AlarmClock,
+ BrickWall,
+ CalendarX,
+ Cloud,
+ PowerCircle,
+ Radio,
+ ServerCrash,
+ Zap,
+ ZapOff
+} from 'lucide-react';
interface UsersStatusType {
[key: string]: StatusType;
@@ -13,11 +23,6 @@ export const UsersStatus: UsersStatusType = {
label: "Active",
variant: "positive"
},
- disabled: {
- icon: PowerOff,
- label: "Disabled",
- variant: "disabled"
- },
limited: {
icon: BrickWall,
label: "Limited",
diff --git a/dashboard/src/features/users/dialogs/settings/user-info/index.ts b/dashboard/src/features/users/dialogs/settings/user-info/index.ts
@@ -1,2 +1,3 @@
export * from './table'
+export * from './user-status-enable-button'
export * from './qrcode'
diff --git a/dashboard/src/features/users/dialogs/settings/user-info/table.tsx b/dashboard/src/features/users/dialogs/settings/user-info/table.tsx
@@ -1,14 +1,25 @@
-import { Button, Card, CardContent, CardHeader, CardTitle, Table, TableBody, TableCell, TableRow } from "@marzneshin/components";
+import {
+ Button,
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ Table,
+ TableBody,
+ TableCell,
+ TableRow
+} from "@marzneshin/components";
import { FC } from 'react';
import {
useUserUsageResetCmd,
UserType,
UsersStatus,
- UsersStatusBadge
+ UsersStatusBadge,
} from '@marzneshin/features/users';
import { CircularProgress } from "@nextui-org/progress";
import { useTranslation } from "react-i18next";
import { format, isDate, isValid } from "date-fns";
+import { UserStatusEnableButton } from "./user-status-enable-button";
interface UserInfoTableProps {
entity: UserType;
@@ -42,13 +53,17 @@ export const UserInfoTable: FC<UserInfoTableProps> = ({ entity }) => {
const { t } = useTranslation();
const { mutate: resetUsage } = useUserUsageResetCmd()
const expireDate = entity.expire ? (isDate(entity.expire) ? entity.expire : new Date(entity.expire)) : null;
+
return (
<Card>
<CardHeader className="flex flex-row justify-between items-center w-full">
<CardTitle>{t('user_info')}</CardTitle>
- <Button className="w-[7rem]" onClick={() => resetUsage(entity)}>
- {t('page.users.reset_usage')}
- </Button>
+ <div className="flex flex-row justify-center items-center gap-3">
+ <Button onClick={() => resetUsage(entity)}>
+ {t('page.users.reset_usage')}
+ </Button>
+ <UserStatusEnableButton user={entity} />
+ </div>
</CardHeader>
<CardContent>
<Table>
diff --git a/dashboard/src/features/users/dialogs/settings/user-info/user-status-enable-button.tsx b/dashboard/src/features/users/dialogs/settings/user-info/user-status-enable-button.tsx
@@ -0,0 +1,32 @@
+import { Button } from "@marzneshin/components";
+import { UserType, useUserStatusEnable } from "@marzneshin/features/users";
+import { LoaderIcon } from "lucide-react";
+import { FC, useCallback, useState } from "react";
+import { useTranslation } from "react-i18next";
+
+interface UserStatusEnableButtonProps {
+ user: UserType;
+}
+
+export const UserStatusEnableButton: FC<UserStatusEnableButtonProps> = ({ user }) => {
+ const [userStatus, setUserStatus] = useState<boolean>(user.enabled)
+ const { t } = useTranslation()
+ const { mutate: userStatusEnable, isPending } = useUserStatusEnable()
+
+ const handleUserStatusEnabledToggle = useCallback(() => {
+ const tempUserStatus = userStatus;
+ userStatusEnable({ user: user, enabled: !userStatus })
+ setUserStatus(!tempUserStatus)
+ }, [user, userStatus, userStatusEnable]);
+
+ const bgColor = isPending ? 'bg-muted-foreground' : (userStatus ? 'bg-red-400' : 'bg-green-400')
+
+ return (
+ <Button
+ className={bgColor}
+ onClick={handleUserStatusEnabledToggle}
+ >
+ {isPending ? <LoaderIcon className="animate-spin" /> : t(!userStatus ? 'enable' : 'disable')}
+ </Button>
+ )
+}
diff --git a/dashboard/src/features/users/services/index.ts b/dashboard/src/features/users/services/index.ts
@@ -4,3 +4,4 @@ export * from './create-user.mutate'
export * from './delete-user.mutate'
export * from './user-usage-reset.command'
export * from './user-subscription-revoke.command'
+export * from './user-status.mutate'
diff --git a/dashboard/src/features/users/services/user-status.mutate.ts b/dashboard/src/features/users/services/user-status.mutate.ts
@@ -0,0 +1,49 @@
+import { useMutation } from "@tanstack/react-query";
+import { fetch, queryClient } from "@marzneshin/utils";
+import { toast } from "sonner";
+import i18n from "@marzneshin/features/i18n";
+import {
+ UserMutationType,
+} from "@marzneshin/features/users";
+
+
+interface UsersStatusEnabledQuery {
+ user: UserMutationType;
+ enabled: boolean;
+}
+
+export async function userStatusEnabled({ user, enabled }: UsersStatusEnabledQuery): Promise<UserMutationType> {
+ const action = enabled ? 'enable' : 'disable'
+ return fetch(`/users/${user.username}/${action}`, { method: 'post' }).then((user) => {
+ return user;
+ });
+}
+
+const handleError = (error: Error, value: UsersStatusEnabledQuery) => {
+ toast.error(
+ i18n.t('events.user_status.error', { name: value.user.username }),
+ {
+ description: error.message
+ })
+}
+
+const handleSuccess = (value: UserMutationType) => {
+ toast.success(
+ i18n.t('events.user_status.success.title', { name: value.username }),
+ {
+ description: i18n.t('events.user_status.success.desc')
+ })
+ queryClient.invalidateQueries({ queryKey: [UsersStatusEnabledFetchKey] })
+}
+
+
+const UsersStatusEnabledFetchKey = "users";
+
+export const useUserStatusEnable = () => {
+ return useMutation({
+ mutationKey: [UsersStatusEnabledFetchKey],
+ mutationFn: userStatusEnabled,
+ onError: handleError,
+ onSuccess: handleSuccess,
+ })
+}
diff --git a/dashboard/src/features/users/types/index.ts b/dashboard/src/features/users/types/index.ts
@@ -24,6 +24,7 @@ export interface UserType {
data_limit: number | null;
data_limit_reset_strategy: DataLimitResetStrategy;
lifetime_used_traffic: number;
+ enabled: boolean;
username: string;
used_traffic: number;
status: Status;