Skip to content

Commit 419ea88

Browse files
rajbosCopilot
andcommitted
feat(sharing-server): add ADMIN_GITHUB_LOGINS env var for declarative admin management
- Add syncAdminLogins() to db.ts: reads ADMIN_GITHUB_LOGINS (comma-separated), grants is_admin=1 to listed logins and revokes from all others (authoritative sync) - Apply admin grant in upsertUser() so new users get the role on first login - Restructure server.ts startup: restore -> await initDbWithRetry -> syncAdminLogins -> serve; fixes a pre-existing race where requests could arrive before DB was ready - initDbWithRetry now throws after exhausting retries instead of silently continuing - Wire ADMIN_GITHUB_LOGINS as a dynamic env var in Terraform (not set when empty, matching the GITHUB_ORG_CHECK_TOKEN pattern) - Add admin_github_logins variable to infra/variables.tf - Document in .env.example and README.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c1e5ec0 commit 419ea88

6 files changed

Lines changed: 72 additions & 12 deletions

File tree

sharing-server/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,9 @@ ALLOWED_GITHUB_ORG=
3434
# 3. Click "Configure SSO" next to the token and authorize it for ALLOWED_GITHUB_ORG
3535
# 4. Paste the generated token below
3636
GITHUB_ORG_CHECK_TOKEN=
37+
38+
# Optional: comma-separated GitHub logins to auto-grant admin access on the dashboard.
39+
# When set, this list is authoritative: listed users get is_admin=1, all others get is_admin=0.
40+
# Leave empty (or unset) to manage admin access manually via the SQLite database.
41+
# Example: ADMIN_GITHUB_LOGINS=alice,bob
42+
ADMIN_GITHUB_LOGINS=

sharing-server/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ Open `http://localhost:3000/dashboard` in your browser to test the OAuth login f
167167
| `PORT` | ❌ | HTTP port (default: `3000`) |
168168
| `DB_PATH` | ❌ | SQLite database path (default: `/data/sharing.db`) |
169169
| `ALLOWED_GITHUB_ORG` | ❌ | If set, only members of this GitHub org can upload data |
170+
| `ADMIN_GITHUB_LOGINS` | ❌ | Comma-separated GitHub logins to auto-grant admin access (e.g. `alice,bob`). When set, this list is authoritative: listed users get admin, all others do not. Leave unset to manage admins manually via SQLite. |
170171

171172
## REST API
172173

sharing-server/infra/main.tf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@ resource "azurerm_container_app" "this" {
161161
secret_name = "org-check-token"
162162
}
163163
}
164+
# Only set ADMIN_GITHUB_LOGINS when a value is provided.
165+
dynamic "env" {
166+
for_each = var.admin_github_logins != "" ? [1] : []
167+
content {
168+
name = "ADMIN_GITHUB_LOGINS"
169+
value = var.admin_github_logins
170+
}
171+
}
164172

165173
liveness_probe {
166174
path = "/health"

sharing-server/infra/variables.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ variable "min_replicas" {
6262
default = 1
6363
}
6464

65+
variable "admin_github_logins" {
66+
description = "Optional: comma-separated GitHub logins to auto-grant admin access (e.g. 'alice,bob'). When set, this list is authoritative — listed users get is_admin=1, all others get is_admin=0. Leave empty to manage admin access manually via the SQLite database."
67+
type = string
68+
default = ""
69+
}
70+
6571
variable "tags" {
6672
description = "Azure resource tags"
6773
type = map(string)

sharing-server/src/db.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,26 @@ function initSchema(db: DatabaseSync): void {
227227
}
228228
}
229229

230+
/** Returns the parsed ADMIN_GITHUB_LOGINS env var as an array of lowercase logins. */
231+
function getAdminLoginsFromEnv(): string[] {
232+
const raw = process.env.ADMIN_GITHUB_LOGINS ?? '';
233+
return raw.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
234+
}
235+
236+
/**
237+
* Sync admin status for all existing users based on ADMIN_GITHUB_LOGINS.
238+
* When the env var is set and non-empty, it is authoritative: users in the list
239+
* get is_admin=1, all others get is_admin=0. When unset or empty, no changes are made.
240+
*/
241+
export function syncAdminLogins(): void {
242+
const logins = getAdminLoginsFromEnv();
243+
if (logins.length === 0) return;
244+
const db = getDb();
245+
const placeholders = logins.map(() => '?').join(', ');
246+
db.prepare(`UPDATE users SET is_admin = CASE WHEN LOWER(github_login) IN (${placeholders}) THEN 1 ELSE 0 END`).run(...logins);
247+
console.log(`[db] Admin sync from ADMIN_GITHUB_LOGINS: ${logins.join(', ')}`);
248+
}
249+
230250
export function upsertUser(
231251
githubId: number,
232252
login: string,
@@ -243,6 +263,11 @@ export function upsertUser(
243263
avatar_url = excluded.avatar_url,
244264
last_seen_at = datetime('now')
245265
`).run(githubId, login, name, avatarUrl);
266+
// Apply env-var admin grant immediately so new users get the right role on first login.
267+
const adminLogins = getAdminLoginsFromEnv();
268+
if (adminLogins.includes(login.toLowerCase())) {
269+
db.prepare('UPDATE users SET is_admin = 1 WHERE github_id = ?').run(githubId);
270+
}
246271
return db.prepare('SELECT * FROM users WHERE github_id = ?').get(githubId) as unknown as UserRow;
247272
}
248273

sharing-server/src/server.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { serve } from '@hono/node-server';
22
import { Hono } from 'hono';
33
import { api } from './routes/api.js';
44
import { dashboard } from './routes/dashboard.js';
5-
import { getDb, closeDb, restoreFromBackup, backupToAzureFiles } from './db.js';
5+
import { getDb, closeDb, restoreFromBackup, backupToAzureFiles, syncAdminLogins } from './db.js';
66

77
const app = new Hono();
88

@@ -35,8 +35,6 @@ function shutdown(signal: string): void {
3535
process.on('SIGTERM', () => shutdown('SIGTERM'));
3636
process.on('SIGINT', () => shutdown('SIGINT'));
3737

38-
const PORT = parseInt(process.env.PORT ?? '3000', 10);
39-
4038
async function initDbWithRetry(maxAttempts = 20): Promise<void> {
4139
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4240
try {
@@ -50,24 +48,40 @@ async function initDbWithRetry(maxAttempts = 20): Promise<void> {
5048
await new Promise(r => setTimeout(r, delay));
5149
} else {
5250
console.error('[db] All init attempts exhausted:', err);
51+
throw err;
5352
}
5453
}
5554
}
5655
}
5756

58-
serve({ fetch: app.fetch, port: PORT }, (info) => {
59-
const org = process.env.ALLOWED_GITHUB_ORG;
60-
console.log(`Token Tracker sharing server listening on port ${info.port}`);
61-
if (org) {
62-
console.log(` Access restricted to members of GitHub org: ${org}`);
63-
} else {
64-
console.log(' Access: open to any GitHub user (set ALLOWED_GITHUB_ORG to restrict)');
65-
}
57+
const PORT = parseInt(process.env.PORT ?? '3000', 10);
58+
59+
async function main(): Promise<void> {
6660
// Restore database from Azure Files backup before opening SQLite.
6761
// SQLite runs on local container disk (/tmp/db) to avoid Azure Files SMB
6862
// locking issues. Azure Files is used only as a backup/restore store.
6963
restoreFromBackup();
70-
initDbWithRetry();
64+
await initDbWithRetry();
65+
syncAdminLogins();
7166
// Periodic backup every 5 minutes in case of unexpected SIGKILL.
7267
setInterval(() => backupToAzureFiles(), 5 * 60 * 1000).unref();
68+
69+
const org = process.env.ALLOWED_GITHUB_ORG;
70+
const adminLogins = process.env.ADMIN_GITHUB_LOGINS;
71+
serve({ fetch: app.fetch, port: PORT }, (info) => {
72+
console.log(`Token Tracker sharing server listening on port ${info.port}`);
73+
if (org) {
74+
console.log(` Access restricted to members of GitHub org: ${org}`);
75+
} else {
76+
console.log(' Access: open to any GitHub user (set ALLOWED_GITHUB_ORG to restrict)');
77+
}
78+
if (adminLogins) {
79+
console.log(` Admin logins (ADMIN_GITHUB_LOGINS): ${adminLogins}`);
80+
}
81+
});
82+
}
83+
84+
main().catch(err => {
85+
console.error('Fatal startup error:', err);
86+
process.exit(1);
7387
});

0 commit comments

Comments
 (0)