diff --git a/app/db/crud.py b/app/db/crud.py index 642c35b..190e163 100644 --- 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 index 7684108..5f4f674 100644 --- 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 new file mode 100644 index 0000000..d1b2300 --- /dev/null +++ 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 index 3828874..4863c77 100644 --- 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 index 84773d2..57e65f9 100644 --- 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 index 6b326d6..c6894ba 100644 --- 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 index 0424063..625b7b7 100644 --- 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 index acc5b5e..eb2a8fa 100644 --- 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 index ab82225..fc2b0ca 100644 --- 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 index d947f5c..74edbfb 100644 --- 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 index a11efe7..d4de22e 100644 --- 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 = ({ entity }) => { const { t } = useTranslation(); const { mutate: resetUsage } = useUserUsageResetCmd() const expireDate = entity.expire ? (isDate(entity.expire) ? entity.expire : new Date(entity.expire)) : null; + return ( {t('user_info')} - +
+ + +
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 new file mode 100644 index 0000000..0476a3e --- /dev/null +++ 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 = ({ user }) => { + const [userStatus, setUserStatus] = useState(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 ( + + ) +} diff --git a/dashboard/src/features/users/services/index.ts b/dashboard/src/features/users/services/index.ts index 7f180cc..3887a8b 100644 --- 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 new file mode 100644 index 0000000..c6eb383 --- /dev/null +++ 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 { + 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 index d4adf7e..fd0ce5b 100644 --- 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;