mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-28 15:47:50 +09:00
Every NOT NULL FK from the KNOWN_DANGLING_ON_DELETE audit now declares its parent-side relationship, so the delete-cascade policy door covers it: AI conversations/pending actions, API keys, dashboards, event subscriptions, login links, sent invitations, resource grants (users); env vars + history, previews + settings, WAF policy (applications); surveys, doctor results, proxy stack, tunnels (servers); manifests (projects); DDNS hosts (zones); exposed services (tunnels); drills (backup policies); rollouts (agent versions); site plugin installs (WordPress sites); queue messages (groups). The ratchet set is now EMPTY and stays that way. One new deliberate exception: Workspace.projects — projects carry environments and app groupings, so delete_workspace now refuses (409) while any remain instead of silently destroying them (previously it crashed with IntegrityError). Migration 095 sweeps rows already orphaned on existing installs by pre-fix hard-deletes — including an application's leftover env-var secrets, which a reused SQLite row id could have attached to a future app. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
59 lines
2.3 KiB
Python
59 lines
2.3 KiB
Python
from datetime import datetime
|
|
|
|
from app import db
|
|
from app.models.mixins import uuid_pk, TimestampMixin
|
|
|
|
|
|
class ExposedService(TimestampMixin, db.Model):
|
|
"""A service on a private (NAT'd) host published to the public internet
|
|
over a WireGuard tunnel (roadmap Phase 2). The edge runs an nginx vhost
|
|
for ``hostname`` that proxies to the private peer's WG IP; the private
|
|
agent forwards that to the real local service on ``port``.
|
|
See docs/REMOTE_ACCESS_ROADMAP.md.
|
|
"""
|
|
__tablename__ = 'exposed_services'
|
|
|
|
id = uuid_pk()
|
|
tunnel_id = db.Column(db.String(36), db.ForeignKey('tunnels.id'), nullable=False, index=True)
|
|
|
|
hostname = db.Column(db.String(255), nullable=False) # e.g. jellyfin.example.com
|
|
port = db.Column(db.Integer, nullable=False) # service port on the private host (e.g. 8096)
|
|
nginx_site_name = db.Column(db.String(120)) # the edge vhost we created
|
|
|
|
require_auth = db.Column(db.Boolean, default=False)
|
|
auth_username = db.Column(db.String(100))
|
|
ssl_enabled = db.Column(db.Boolean, default=False)
|
|
|
|
status = db.Column(db.String(20), default='pending', index=True) # pending / published / error
|
|
last_error = db.Column(db.Text)
|
|
|
|
|
|
# Parent-side backref so the delete-cascade policy covers the NOT NULL FK.
|
|
tunnel = db.relationship('Tunnel',
|
|
backref=db.backref('exposed_services', lazy='dynamic'))
|
|
|
|
def url(self):
|
|
if not self.hostname:
|
|
return None
|
|
return ('https://' if self.ssl_enabled else 'http://') + self.hostname
|
|
|
|
def to_dict(self):
|
|
return {
|
|
'id': self.id,
|
|
'tunnel_id': self.tunnel_id,
|
|
'hostname': self.hostname,
|
|
'port': self.port,
|
|
'nginx_site_name': self.nginx_site_name,
|
|
'require_auth': self.require_auth,
|
|
'auth_username': self.auth_username,
|
|
'ssl_enabled': self.ssl_enabled,
|
|
'status': self.status,
|
|
'last_error': self.last_error,
|
|
'url': self.url(),
|
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
def __repr__(self):
|
|
return '<ExposedService %s -> tunnel %s :%s>' % (self.hostname, self.tunnel_id, self.port)
|