gitscope
← marzneshin / v0.4.1 / app/db/models.py
https://dev.kronoz.win/r/marzneshin.git
app/db/models.py · Python
Blame Raw
  1import os
  2import secrets
  3from datetime import datetime
  4
  5import sqlalchemy.sql
  6from sqlalchemy import (
  7    BigInteger,
  8    Boolean,
  9    Column,
 10    DateTime,
 11    Enum,
 12    Float,
 13    ForeignKey,
 14    Integer,
 15    String,
 16    Table,
 17    UniqueConstraint,
 18    JSON,
 19    and_,
 20    func,
 21    select,
 22)
 23from sqlalchemy.ext.hybrid import hybrid_property
 24from sqlalchemy.orm import relationship, column_property
 25from sqlalchemy.sql.expression import text
 26
 27from app.config.env import SUBSCRIPTION_URL_PREFIX
 28from app.db.base import Base
 29from app.models.node import NodeStatus
 30from app.models.proxy import (
 31    InboundHostFingerprint,
 32    InboundHostSecurity,
 33    ProxyTypes,
 34)
 35from app.models.user import (
 36    ReminderType,
 37    UserDataUsageResetStrategy,
 38    UserStatus,
 39    UserExpireStrategy,
 40)
 41
 42admins_services = Table(
 43    "admins_services",
 44    Base.metadata,
 45    Column("admin_id", ForeignKey("admins.id"), primary_key=True),
 46    Column("service_id", ForeignKey("services.id"), primary_key=True),
 47)
 48
 49inbounds_services = Table(
 50    "inbounds_services",
 51    Base.metadata,
 52    Column("inbound_id", ForeignKey("inbounds.id"), primary_key=True),
 53    Column("service_id", ForeignKey("services.id"), primary_key=True),
 54)
 55
 56users_services = Table(
 57    "users_services",
 58    Base.metadata,
 59    Column("user_id", ForeignKey("users.id"), primary_key=True),
 60    Column("service_id", ForeignKey("services.id"), primary_key=True),
 61)
 62
 63
 64class Admin(Base):
 65    __tablename__ = "admins"
 66
 67    id = Column(Integer, primary_key=True)
 68    username = Column(String(32), unique=True, index=True)
 69    hashed_password = Column(String(128))
 70    users = relationship("User", back_populates="admin")
 71    services = relationship(
 72        "Service",
 73        secondary=admins_services,
 74        back_populates="admins",
 75        lazy="joined",
 76    )
 77    enabled = Column(
 78        Boolean,
 79        nullable=False,
 80        default=True,
 81        server_default=sqlalchemy.sql.true(),
 82    )
 83    all_services_access = Column(
 84        Boolean,
 85        nullable=False,
 86        default=False,
 87        server_default=sqlalchemy.sql.false(),
 88    )
 89    modify_users_access = Column(
 90        Boolean,
 91        nullable=False,
 92        default=True,
 93        server_default=sqlalchemy.sql.true(),
 94    )
 95    created_at = Column(DateTime, default=datetime.utcnow)
 96    is_sudo = Column(Boolean, default=False)
 97    password_reset_at = Column(DateTime)
 98    subscription_url_prefix = Column(
 99        String(256),
100        nullable=False,
101        default="",
102        server_default=sqlalchemy.sql.text(""),
103    )
104
105    @property
106    def service_ids(self):
107        return [service.id for service in self.services]
108
109    @classmethod
110    def __declare_last__(cls):
111        cls.users_data_usage = column_property(
112            select(func.coalesce(func.sum(User.lifetime_used_traffic), 0))
113            .where(User.admin_id == cls.id)
114            .correlate_except(User)
115            .scalar_subquery()
116        )
117
118
119class Service(Base):
120    __tablename__ = "services"
121    id = Column(Integer, primary_key=True)
122    name = Column(String(64))
123    admins = relationship(
124        "Admin", secondary=admins_services, back_populates="services"
125    )
126    users = relationship(
127        "User", secondary=users_services, back_populates="services"
128    )
129    inbounds = relationship(
130        "Inbound", secondary=inbounds_services, back_populates="services"
131    )
132
133    @property
134    def inbound_ids(self):
135        return [inbound.id for inbound in self.inbounds]
136
137    @property
138    def user_ids(self):
139        return [user.id for user in self.users]
140
141
142class User(Base):
143    __tablename__ = "users"
144
145    id = Column(Integer, primary_key=True)
146    username = Column(String(32), unique=True, index=True)
147    key = Column(String(64), unique=True)
148    activated = Column(Boolean, nullable=False, default=True)
149    enabled = Column(
150        Boolean,
151        nullable=False,
152        default=True,
153        server_default=sqlalchemy.sql.true(),
154    )
155    removed = Column(
156        Boolean,
157        nullable=False,
158        default=False,
159        server_default=sqlalchemy.sql.false(),
160    )
161    services = relationship(
162        "Service",
163        secondary=users_services,
164        back_populates="users",
165        lazy="joined",
166    )
167    inbounds = relationship(
168        "Inbound",
169        secondary="join(users_services, inbounds_services, inbounds_services.c.service_id == users_services.c.service_id)"
170        ".join(Inbound, Inbound.id == inbounds_services.c.inbound_id)",
171        viewonly=True,
172        distinct_target_key=True,
173    )
174    used_traffic = Column(BigInteger, default=0)
175    lifetime_used_traffic = Column(
176        BigInteger, default=0, server_default="0", nullable=False
177    )
178    traffic_reset_at = Column(DateTime)
179    node_usages = relationship(
180        "NodeUserUsage",
181        back_populates="user",
182        cascade="all,delete,delete-orphan",
183    )
184    notification_reminders = relationship(
185        "NotificationReminder",
186        back_populates="user",
187        cascade="all,delete,delete-orphan",
188    )
189    data_limit = Column(BigInteger)
190    data_limit_reset_strategy = Column(
191        Enum(UserDataUsageResetStrategy),
192        nullable=False,
193        default=UserDataUsageResetStrategy.no_reset,
194    )
195    ip_limit = Column(Integer, nullable=False, default=-1)
196    settings = Column(String(1024))
197    expire_strategy = Column(
198        Enum(UserExpireStrategy),
199        nullable=False,
200        default=UserExpireStrategy.NEVER,
201    )
202    expire_date = Column(DateTime)
203    usage_duration = Column(BigInteger)
204    activation_deadline = Column(DateTime)
205    admin_id = Column(Integer, ForeignKey("admins.id"))
206    admin = relationship("Admin", back_populates="users")
207    sub_updated_at = Column(DateTime)
208    sub_last_user_agent = Column(String(512))
209    created_at = Column(DateTime, default=datetime.utcnow)
210    note = Column(String(500))
211    online_at = Column(DateTime)
212    edit_at = Column(DateTime)
213
214    @property
215    def service_ids(self):
216        return [service.id for service in self.services]
217
218    @hybrid_property
219    def expired(self):
220        if self.expire_strategy == "fixed_date":
221            return self.expire_date < datetime.utcnow()
222        return False
223
224    @expired.expression
225    def expired(cls):
226        return and_(
227            cls.expire_strategy == "fixed_date", cls.expire_date < func.now()
228        )
229
230    @hybrid_property
231    def data_limit_reached(self):
232        if self.data_limit is not None:
233            return self.used_traffic >= self.data_limit
234        return False
235
236    @data_limit_reached.expression
237    def data_limit_reached(cls):
238        return and_(
239            cls.data_limit.isnot(None), cls.used_traffic >= cls.data_limit
240        )
241
242    @hybrid_property
243    def is_active(self):
244        return (
245            self.enabled
246            and not self.expired
247            and not self.data_limit_reached
248            and not self.removed
249        )
250
251    @is_active.expression
252    def is_active(cls):
253        return and_(
254            cls.enabled == True,
255            ~cls.expired,
256            ~cls.data_limit_reached,
257            ~cls.removed,
258        )
259
260    @property
261    def status(self):
262        return UserStatus.ACTIVE if self.is_active else UserStatus.INACTIVE
263
264    @property
265    def subscription_url(self):
266        prefix = self.admin.subscription_url_prefix or SUBSCRIPTION_URL_PREFIX
267        return (
268            prefix.replace("*", secrets.token_hex(8))
269            + f"/sub/{self.username}/{self.key}"
270        )
271
272    @hybrid_property
273    def owner_username(self):
274        return self.admin.username
275
276
277class Inbound(Base):
278    __tablename__ = "inbounds"
279    __table_args__ = (UniqueConstraint("node_id", "tag"),)
280
281    id = Column(Integer, primary_key=True)
282    protocol = Column(Enum(ProxyTypes))
283    tag = Column(String(256), nullable=False)
284    config = Column(String(512), nullable=False)
285    node_id = Column(Integer, ForeignKey("nodes.id"), index=True)
286    node = relationship("Node", back_populates="inbounds")
287    services = relationship(
288        "Service", secondary=inbounds_services, back_populates="inbounds"
289    )
290    hosts = relationship(
291        "InboundHost",
292        back_populates="inbound",
293        cascade="all, delete, delete-orphan",
294    )
295
296    @property
297    def service_ids(self):
298        return [service.id for service in self.services]
299
300
301class InboundHost(Base):
302    __tablename__ = "hosts"
303
304    id = Column(Integer, primary_key=True)
305    remark = Column(String(256), nullable=False)
306    address = Column(String(256), nullable=False)
307    port = Column(Integer)
308    path = Column(String(256))
309    sni = Column(String(1024))
310    host = Column(String(1024))
311    security = Column(
312        Enum(InboundHostSecurity),
313        nullable=False,
314        default=InboundHostSecurity.inbound_default,
315    )
316    alpn = Column(
317        String(32),
318        server_default=sqlalchemy.sql.null(),
319    )
320    fingerprint = Column(
321        Enum(InboundHostFingerprint),
322        nullable=False,
323        default=InboundHostSecurity.none,
324        server_default=InboundHostSecurity.none.name,
325    )
326
327    mux = Column(
328        Boolean,
329        default=False,
330        nullable=False,
331        server_default=sqlalchemy.sql.false(),
332    )
333    fragment = Column(JSON())
334
335    inbound_id = Column(Integer, ForeignKey("inbounds.id"), nullable=False)
336    inbound = relationship("Inbound", back_populates="hosts")
337    allowinsecure = Column(Boolean, default=False)
338    is_disabled = Column(Boolean, default=False)
339
340
341class System(Base):
342    __tablename__ = "system"
343
344    id = Column(Integer, primary_key=True)
345    uplink = Column(BigInteger, default=0)
346    downlink = Column(BigInteger, default=0)
347
348
349class JWT(Base):
350    __tablename__ = "jwt"
351
352    id = Column(Integer, primary_key=True)
353    secret_key = Column(
354        String(64), nullable=False, default=lambda: os.urandom(32).hex()
355    )
356
357
358class TLS(Base):
359    __tablename__ = "tls"
360
361    id = Column(Integer, primary_key=True)
362    key = Column(String(4096), nullable=False)
363    certificate = Column(String(2048), nullable=False)
364
365
366class Node(Base):
367    __tablename__ = "nodes"
368    __table_args__ = (UniqueConstraint("address", "port"),)
369    id = Column(Integer, primary_key=True)
370    name = Column(String(256), unique=True)
371    connection_backend = Column(String(32))
372    address = Column(String(256))
373    port = Column(Integer)
374    xray_version = Column(String(32))
375    inbounds = relationship(
376        "Inbound", back_populates="node", cascade="all, delete"
377    )
378    status = Column(
379        Enum(NodeStatus), nullable=False, default=NodeStatus.unhealthy
380    )
381    last_status_change = Column(DateTime, default=datetime.utcnow)
382    message = Column(String(1024))
383    created_at = Column(DateTime, default=datetime.utcnow)
384    uplink = Column(BigInteger, default=0)
385    downlink = Column(BigInteger, default=0)
386    user_usages = relationship(
387        "NodeUserUsage",
388        back_populates="node",
389        cascade="all, delete, delete-orphan",
390    )
391    usages = relationship(
392        "NodeUsage",
393        back_populates="node",
394        cascade="all, delete, delete-orphan",
395    )
396    usage_coefficient = Column(
397        Float, nullable=False, server_default=text("1.0"), default=1
398    )
399
400    @property
401    def inbound_ids(self):
402        return [inbound.id for inbound in self.inbounds]
403
404
405class NodeUserUsage(Base):
406    __tablename__ = "node_user_usages"
407    __table_args__ = (UniqueConstraint("created_at", "user_id", "node_id"),)
408
409    id = Column(Integer, primary_key=True)
410    created_at = Column(DateTime, nullable=False)  # one hour per record
411    user_id = Column(Integer, ForeignKey("users.id"))
412    user = relationship("User", back_populates="node_usages")
413    node_id = Column(Integer, ForeignKey("nodes.id"))
414    node = relationship("Node", back_populates="user_usages")
415    used_traffic = Column(BigInteger, default=0)
416
417
418class NodeUsage(Base):
419    __tablename__ = "node_usages"
420    __table_args__ = (UniqueConstraint("created_at", "node_id"),)
421
422    id = Column(Integer, primary_key=True)
423    created_at = Column(DateTime, nullable=False)  # one hour per record
424    node_id = Column(Integer, ForeignKey("nodes.id"))
425    node = relationship("Node", back_populates="usages")
426    uplink = Column(BigInteger, default=0)
427    downlink = Column(BigInteger, default=0)
428
429
430class NotificationReminder(Base):
431    __tablename__ = "notification_reminders"
432
433    id = Column(Integer, primary_key=True)
434    user_id = Column(Integer, ForeignKey("users.id"))
435    user = relationship("User", back_populates="notification_reminders")
436    type = Column(Enum(ReminderType), nullable=False)
437    expires_at = Column(DateTime)
438    created_at = Column(DateTime, default=datetime.utcnow)
439
440
441class Settings(Base):
442    __tablename__ = "settings"
443
444    id = Column(Integer, primary_key=True, server_default=text("0"))
445    subscription = Column(JSON, nullable=False)
446    telegram = Column(JSON)