diff --git a/db/schema.sql b/db/schema.sql
index d979465..fd014c0 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -2,6 +2,13 @@
-- A "model" is one logical download (a Thingiverse thing, a Printables model, a
-- single uploaded STL). A model owns one or more files (the actual .stl/.3mf/...).
+-- Per-license commercial-use policy. Auto-registered as licenses are seen;
+-- editable in Settings. commercial: true=sellable, false=non-commercial, null=unknown.
+CREATE TABLE IF NOT EXISTS license_rules (
+ name TEXT PRIMARY KEY,
+ commercial BOOLEAN
+);
+
-- App users (email/password auth).
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
diff --git a/lib/licenses.js b/lib/licenses.js
index 7a7a489..5b4e569 100644
--- a/lib/licenses.js
+++ b/lib/licenses.js
@@ -11,8 +11,9 @@ function inferCommercial(text) {
if (/non-?commercial|personal use|community use|[-_\s]nc([-_\s]|$)|exclusive|standard digital file|all rights reserved|^\s*none\s*$|charge money|shall not[^.]*\bsell|not be sold/.test(s)) {
return false;
}
- // Clearly permissive (commercial use OK). NC variants are already excluded above.
- if (/cc0|public ?domain|creative commons|attribution|cc[-_\s]?by|^\s*mit\b|^\s*bsd\b|^\s*l?gpl\b|royalty[- ]free|commercial use (is )?(allowed|permitted)|may be sold/.test(s)) {
+ // Clearly permissive (commercial use OK). NC variants are already excluded above,
+ // so any remaining CC "BY" code (by, by-sa, by-nd) allows commercial use.
+ if (/cc0|public ?domain|creative commons|attribution|cc[-_\s]?by|(^|[-_\s])by([-_\s]|$)|by[-_](sa|nd)|share[- ]?alike|^\s*mit\b|^\s*bsd\b|^\s*l?gpl\b|royalty[- ]free|commercial use (is )?(allowed|permitted)|may be sold/.test(s)) {
return true;
}
return null;
diff --git a/lib/store.js b/lib/store.js
index 6627347..9396244 100644
--- a/lib/store.js
+++ b/lib/store.js
@@ -208,6 +208,39 @@ async function deleteProject(id) {
return rowCount > 0;
}
+// ---------------------------------------------------------------- license rules
+async function getLicenseRule(name) {
+ const { rows } = await db.query('SELECT name, commercial FROM license_rules WHERE name = $1', [name]);
+ return rows[0] || null;
+}
+
+async function upsertLicenseRule(name, commercial) {
+ await db.query(
+ `INSERT INTO license_rules (name, commercial) VALUES ($1, $2)
+ ON CONFLICT (name) DO UPDATE SET commercial = EXCLUDED.commercial`,
+ [name, commercial ?? null]);
+}
+
+async function listLicenseRules() {
+ const { rows } = await db.query(`
+ SELECT lr.name, lr.commercial, COUNT(m.id)::int AS model_count
+ FROM license_rules lr
+ LEFT JOIN models m ON m.license = lr.name
+ GROUP BY lr.name, lr.commercial
+ ORDER BY lr.name`);
+ return rows;
+}
+
+async function distinctModelLicenses() {
+ const { rows } = await db.query("SELECT DISTINCT license FROM models WHERE license IS NOT NULL AND license <> ''");
+ return rows.map((r) => r.license);
+}
+
+// Push a rule's value onto every model that carries that license.
+async function applyLicenseRuleToModels(name, commercial) {
+ await db.query('UPDATE models SET commercial_use = $2 WHERE license = $1', [name, commercial ?? null]);
+}
+
// ---------------------------------------------------------------- users / auth
async function countUsers() {
const { rows } = await db.query('SELECT COUNT(*)::int AS n FROM users');
@@ -287,4 +320,5 @@ module.exports = {
getSetting, setSetting,
countUsers, getUserByEmail, getUserById, createUser, updateUserPassword,
createPasswordReset, getValidReset, markResetUsed,
+ getLicenseRule, upsertLicenseRule, listLicenseRules, distinctModelLicenses, applyLicenseRuleToModels,
};
diff --git a/public/app.js b/public/app.js
index f222434..bb59b54 100644
--- a/public/app.js
+++ b/public/app.js
@@ -420,6 +420,7 @@ async function openSettingsModal() {
const s = settings[input.dataset.setting];
if (s && !s.secret && s.value) input.value = s.value;
});
+ await renderLicenseRules();
} catch (err) {
document.getElementById('settingsStatus').textContent = 'Could not load settings: ' + err.message;
}
@@ -776,6 +777,44 @@ async function handleImport(e) {
}
}
+async function renderLicenseRules() {
+ const wrap = document.getElementById('licenseRules');
+ let rules;
+ try { rules = await (await fetch('/api/licenses')).json(); }
+ catch { wrap.textContent = 'Could not load licenses.'; return; }
+ if (!rules.length) { wrap.innerHTML = '
No licenses yet — import some models.
'; return; }
+ const opt = (v, sel) => `