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