From 242154bf5cdb4eb69d07bad375bc7a3b5c64f916 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Fri, 3 Jul 2026 09:38:31 -0600 Subject: [PATCH 1/2] Dev stuff for orgs --- _test_server_temp.js | 73 ++++ database.db.bak | Bin 0 -> 102400 bytes lib/auth.js | 15 +- lib/orgs.js | 39 ++ migrations/0008_organizations.sql | 56 +++ public/css/style.css | 36 ++ public/dashboard/index.html | 97 ++++- public/js/dashboard.js | 434 +++++++++++++++++----- routes/admin.js | 23 +- routes/auth.js | 18 +- routes/dashboard.js | 573 +++++++++++++++++++++--------- 11 files changed, 1084 insertions(+), 280 deletions(-) create mode 100644 _test_server_temp.js create mode 100644 database.db.bak create mode 100644 lib/orgs.js create mode 100644 migrations/0008_organizations.sql diff --git a/_test_server_temp.js b/_test_server_temp.js new file mode 100644 index 0000000..579a8b0 --- /dev/null +++ b/_test_server_temp.js @@ -0,0 +1,73 @@ +// Temporary test harness - deleted after verification. Points at a scratch copy of the DB so the +// real database.db is never touched. +require('dotenv').config({ quiet: true }); +const express = require('express'); +const sqlite3 = require('sqlite3').verbose(); +const app = express(); +const session = require('express-session'); +const port = 3999; + +const db = new sqlite3.Database('/tmp/database_full_test.db', (err) => { + if (err) { + console.error('Failed to connect to the database:', err.message); + process.exit(1); + } else { + console.log('Connected to the SQLite database.'); + require('./migrations')(db).then(() => { + app.listen(port, () => { + console.log(`NOTXCS API is running on port ${port}`); + }); + }).catch(err => { + console.error('Failed to run migrations:', err.message); + process.exit(1); + }); + }; +}); + +app.use(express.static('public')); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(session({ + secret: 'test-secret', + resave: false, + saveUninitialized: true, + cookie: { secure: false } +})); + +app.use((req, res, next) => { + console.log(`${req.method} ${req.originalUrl}`); + next(); +}); + +const fs = require('fs'); +const path = require('path'); + +const routesPath = path.join(__dirname, 'routes'); + +function loadRoutes(dir, basePath = '/') { + const files = fs.readdirSync(dir); + + files.forEach((file) => { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + loadRoutes(fullPath, path.join(basePath, file)); + } else if (file.endsWith('.js')) { + const routeName = path.basename(file, '.js'); + const routeUrl = path.join(basePath, routeName === 'index' ? '' : routeName).replace(/\\/g, '/'); + + try { + const route = require(fullPath)(db); + if (typeof route === 'function' || route.name === 'router') { + app.use(routeUrl, route); + console.log(`Loaded route: ${routeUrl}`); + } + } catch (err) { + console.error(`Error loading route ${fullPath}:`, err); + } + } + }); +} + +loadRoutes(routesPath); diff --git a/database.db.bak b/database.db.bak new file mode 100644 index 0000000000000000000000000000000000000000..be3e0d66953d1bb75bd6f52aa73e4c788ddeb73e GIT binary patch literal 102400 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 RX>>-hpkvv?{J&*M{$E}sS498- literal 0 HcmV?d00001 diff --git a/lib/auth.js b/lib/auth.js index cc6db11..17cdde1 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -12,6 +12,19 @@ const ROLES = { SUPERADMIN: 4 }; +// Organization membership roles. These are scoped to a single organization only and are completely +// independent of the platform-wide `ROLES` above (e.g. an org Admin is not a platform admin). +// 1 = View: read-only access to the org's places, readers, ACL entries, access groups and logs. +// 2 = Edit: everything View can do, plus create/edit/delete places, readers, ACL entries and +// access groups, regenerate API keys, and engage/lift place-level lockdowns. +// 3 = Admin: everything Edit can do, plus manage organization membership (add/remove members, +// change roles), rename the organization, and delete it (once it owns no places). +const ORG_ROLES = { + VIEW: 1, + EDIT: 2, + ADMIN: 3 +}; + // Returns Express middleware that requires an authenticated session and (optionally) a minimum role. // `getDb` is a function returning the current db instance, since route modules assign `db` after this // middleware is registered (see routes/dashboard.js and routes/admin.js). @@ -45,4 +58,4 @@ function requireAuth(getDb, minRole = ROLES.USER) { }; } -module.exports = { ROLES, requireAuth }; +module.exports = { ROLES, ORG_ROLES, requireAuth }; diff --git a/lib/orgs.js b/lib/orgs.js new file mode 100644 index 0000000..0ec11a0 --- /dev/null +++ b/lib/orgs.js @@ -0,0 +1,39 @@ +const { ROLES, ORG_ROLES } = require('./auth'); + +// Creates a new default personal organization for a freshly-created user and makes them an Admin +// member of it. Used both at registration (routes/auth.js) and when a platform admin creates a user +// directly (routes/admin.js), so every user always has at least one organization to create places in. +function createDefaultOrg(db, userId, username, callback) { + db.run(`INSERT INTO organizations (name, ownerId) VALUES (?, ?)`, [`Personal (${username})`, userId], function (err) { + if (err) return callback(err); + + const orgId = this.lastID; + db.run(`INSERT INTO organization_members (orgId, userId, role, invitedBy) VALUES (?, ?, ?, ?)`, [orgId, userId, ORG_ROLES.ADMIN, userId], (err) => { + if (err) return callback(err); + callback(null, orgId); + }); + }); +} + +// Looks up the caller's effective role within an organization: their actual membership role if +// they're a member, or an Admin-level "bypass" if they're not a member but hold a platform-wide +// elevated/admin/superadmin role (mirroring the old per-place ownership bypass). Calls back with +// `{ role, isMember, bypass }`, or `{ role: null }` if the user has no access at all. +function getEffectiveOrgRole(db, orgId, user, callback) { + db.get(`SELECT role FROM organization_members WHERE orgId = ? AND userId = ?`, [orgId, user.id], (err, member) => { + if (err) return callback(err); + + if (member) { + return callback(null, { role: member.role, isMember: true, bypass: false }); + } + + if (user.role >= ROLES.ELEVATED) { + return callback(null, { role: ORG_ROLES.ADMIN, isMember: false, bypass: true }); + } + + callback(null, { role: null, isMember: false, bypass: false }); + }); +} + +module.exports = { createDefaultOrg, getEffectiveOrgRole }; + diff --git a/migrations/0008_organizations.sql b/migrations/0008_organizations.sql new file mode 100644 index 0000000..2cbaddf --- /dev/null +++ b/migrations/0008_organizations.sql @@ -0,0 +1,56 @@ +-- Organizations replace per-place shared access: every place belongs to an organization, and access +-- to it is determined by the caller's role/membership in that organization rather than by direct +-- ownership or one-off per-place grants (see the now-dropped place_access table, below). +CREATE TABLE IF NOT EXISTS organizations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + ownerId INTEGER, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(ownerId) REFERENCES users(id) +); + +-- Membership roles are scoped to the organization only (independent of the platform-wide role in +-- users.role): 1 = View (read-only), 2 = Edit (manage places/readers/ACL/access groups), 3 = Admin +-- (Edit, plus manage organization membership/roles and rename/delete the organization itself). +CREATE TABLE IF NOT EXISTS organization_members ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + orgId INTEGER NOT NULL, + userId INTEGER NOT NULL, + role INTEGER NOT NULL DEFAULT 1, + invitedBy INTEGER, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(orgId) REFERENCES organizations(id), + FOREIGN KEY(userId) REFERENCES users(id), + FOREIGN KEY(invitedBy) REFERENCES users(id), + UNIQUE(orgId, userId) +); + +CREATE INDEX IF NOT EXISTS idx_org_members_userId ON organization_members(userId); + +CREATE INDEX IF NOT EXISTS idx_org_members_orgId ON organization_members(orgId); + +ALTER TABLE places ADD COLUMN orgId INTEGER REFERENCES organizations(id); + +-- Auto-create a default personal organization for every existing user. +INSERT INTO organizations (name, ownerId) +SELECT 'Personal (' || username || ')', id FROM users; + +-- Make every user an Admin member of their own new default organization. +INSERT INTO organization_members (orgId, userId, role, invitedBy) +SELECT organizations.id, organizations.ownerId, 3, organizations.ownerId +FROM organizations; + +-- Move every personally-owned place into its owner's new default organization. +UPDATE places +SET orgId = (SELECT id FROM organizations WHERE organizations.ownerId = places.ownerId) +WHERE ownerId IS NOT NULL; + +-- Carry over any existing per-place shared access grants as Edit members of the place's new +-- organization, then drop the now-unused table - organizations replace this feature entirely. +INSERT OR IGNORE INTO organization_members (orgId, userId, role, invitedBy, createdAt) +SELECT places.orgId, place_access.userId, 2, place_access.grantedBy, place_access.createdAt +FROM place_access +JOIN places ON places.id = place_access.placeId +WHERE places.orgId IS NOT NULL; + +DROP TABLE IF EXISTS place_access; diff --git a/public/css/style.css b/public/css/style.css index 97b7882..28c9e2a 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -174,6 +174,42 @@ button.danger { margin-bottom: 8px; } +.org-switcher { + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border); +} + +.org-switcher label { + margin-bottom: 6px; +} + +.org-switcher-row { + display: flex; + gap: 8px; +} + +.org-switcher-row select { + flex: 1; + margin-bottom: 0; +} + +.org-switcher-row button { + white-space: nowrap; +} + +.role-pill { + display: inline-block; + padding: 2px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + background: rgba(79, 124, 255, 0.15); + color: var(--accent); + white-space: nowrap; +} + + .place-list { list-style: none; margin: 0; diff --git a/public/dashboard/index.html b/public/dashboard/index.html index f80e86c..329499c 100644 --- a/public/dashboard/index.html +++ b/public/dashboard/index.html @@ -11,12 +11,22 @@ +
-
-

Shared access

-

Grant other users access to manage this place's settings, readers and access groups.

- +
+

Move to another organization

+

Move this place to a different organization you have Edit access in. Everyone in the current organization will lose access to it.

+
- - + +
- + - -
    +