mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 21:43:05 +09:00
Themes are data, not code — so the backend is a simple validated-JSON store, no zips/checksums/permissions. - Theme model + migration 078_themes - theme_tokens.py: server-side mirror of the token whitelist + validators (the gate: a malicious/malformed theme never reaches the DB) - theme_service: bundled-seed loading, import/list/delete, panel default, pre-auth public/active - /api/v1/themes: /installed, /import (admin), DELETE (admin), /default get+set (admin), unauthenticated /public/active for login/setup, plus /registry + /registry/<slug>/install - theme_registry_service: offline-tolerant registry fetch (TTL + last-good + bundled fallback), mirrors the extension registry pattern - bundled seed themes synced to app/data/themes + themes_index.json - 18 proving tests (server-side validation sanitizes url()/;/} smuggling, admin gate, panel-default → public/active, offline fallback, install flow) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57 lines
2.5 KiB
Python
57 lines
2.5 KiB
Python
"""Theme model (plan 60).
|
|
|
|
A theme is DATA, not code: a validated map of CSS custom-property tokens plus
|
|
gallery metadata. Installed themes (imported, studio-authored, or pulled from
|
|
the registry) live here; the always-present bundled seed themes ship as JSON
|
|
under ``app/data/themes/`` and are merged in by ``theme_service`` — they are not
|
|
rows unless an admin explicitly imports one.
|
|
|
|
No zips, no sha256, no permissions: reviewing a theme is reading a diff of color
|
|
values, so themes deliberately bypass the whole extension trust machinery.
|
|
"""
|
|
from datetime import datetime
|
|
|
|
from app import db
|
|
|
|
|
|
class Theme(db.Model):
|
|
__tablename__ = 'themes'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
slug = db.Column(db.String(64), nullable=False, unique=True, index=True)
|
|
name = db.Column(db.String(120), nullable=False)
|
|
author = db.Column(db.String(120), nullable=True, default='')
|
|
version = db.Column(db.String(32), nullable=True, default='1.0.0')
|
|
description = db.Column(db.Text, nullable=True, default='')
|
|
# 'dark' | 'light' — which mode this theme is primarily a skin of.
|
|
base = db.Column(db.String(8), nullable=False, default='dark')
|
|
# { "dark": {token: value, …}, "light": {…} } — already sanitised.
|
|
tokens = db.Column(db.JSON, nullable=False, default=dict)
|
|
# Optional single accent hex; ThemeContext derives the ramp.
|
|
accent = db.Column(db.String(32), nullable=True)
|
|
# 4 gallery swatches.
|
|
preview = db.Column(db.JSON, nullable=True, default=list)
|
|
# Where it came from: 'registry' | 'import' | 'studio' | 'bundled'.
|
|
source = db.Column(db.String(16), nullable=False, default='import')
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
def to_dict(self):
|
|
return {
|
|
'schema_version': 1,
|
|
'slug': self.slug,
|
|
'name': self.name,
|
|
'author': self.author or '',
|
|
'version': self.version or '1.0.0',
|
|
'description': self.description or '',
|
|
'base': self.base or 'dark',
|
|
'tokens': self.tokens or {},
|
|
'accent': self.accent,
|
|
'preview': self.preview or [],
|
|
'source': self.source,
|
|
'builtin': False,
|
|
'installed': True,
|
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|