mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-03 00:29:10 +00:00
feat: Add setup token security for initial server setup
- Add setupTokens database table with proper schema - Implement setup token generation on first server startup - Add token validation endpoint and modify admin creation - Update initial setup page to require setup token - Add migration scripts for both SQLite and PostgreSQL - Add internationalization support for setup token fields - Implement proper error handling and logging - Add CLI command for resetting user security keys This prevents unauthorized access during initial server setup by requiring a token that is generated and displayed in the server console.
This commit is contained in:
73
server/setup/ensureSetupToken.ts
Normal file
73
server/setup/ensureSetupToken.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { db, setupTokens, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
|
||||
import moment from "moment";
|
||||
import logger from "@server/logger";
|
||||
|
||||
const random: RandomReader = {
|
||||
read(bytes: Uint8Array): void {
|
||||
crypto.getRandomValues(bytes);
|
||||
}
|
||||
};
|
||||
|
||||
function generateToken(): string {
|
||||
// Generate a 32-character alphanumeric token
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
return generateRandomString(random, alphabet, 32);
|
||||
}
|
||||
|
||||
function generateId(length: number): string {
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
return generateRandomString(random, alphabet, length);
|
||||
}
|
||||
|
||||
export async function ensureSetupToken() {
|
||||
try {
|
||||
// Check if a server admin already exists
|
||||
const [existingAdmin] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.serverAdmin, true));
|
||||
|
||||
// If admin exists, no need for setup token
|
||||
if (existingAdmin) {
|
||||
logger.warn("Server admin exists. Setup token generation skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if a setup token already exists
|
||||
const existingTokens = await db
|
||||
.select()
|
||||
.from(setupTokens)
|
||||
.where(eq(setupTokens.used, false));
|
||||
|
||||
// If unused token exists, display it instead of creating a new one
|
||||
if (existingTokens.length > 0) {
|
||||
console.log("=== SETUP TOKEN EXISTS ===");
|
||||
console.log("Token:", existingTokens[0].token);
|
||||
console.log("Use this token on the initial setup page");
|
||||
console.log("================================");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate a new setup token
|
||||
const token = generateToken();
|
||||
const tokenId = generateId(15);
|
||||
|
||||
await db.insert(setupTokens).values({
|
||||
tokenId: tokenId,
|
||||
token: token,
|
||||
used: false,
|
||||
dateCreated: moment().toISOString(),
|
||||
dateUsed: null
|
||||
});
|
||||
|
||||
console.log("=== SETUP TOKEN GENERATED ===");
|
||||
console.log("Token:", token);
|
||||
console.log("Use this token on the initial setup page");
|
||||
console.log("================================");
|
||||
} catch (error) {
|
||||
console.error("Failed to ensure setup token:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ensureActions } from "./ensureActions";
|
||||
import { copyInConfig } from "./copyInConfig";
|
||||
import { clearStaleData } from "./clearStaleData";
|
||||
import { ensureSetupToken } from "./ensureSetupToken";
|
||||
|
||||
export async function runSetupFunctions() {
|
||||
await copyInConfig(); // copy in the config to the db as needed
|
||||
await ensureActions(); // make sure all of the actions are in the db and the roles
|
||||
await clearStaleData();
|
||||
await ensureSetupToken(); // ensure setup token exists for initial setup
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import path from "path";
|
||||
import m1 from "./scriptsPg/1.6.0";
|
||||
import m2 from "./scriptsPg/1.7.0";
|
||||
import m3 from "./scriptsPg/1.8.0";
|
||||
import m4 from "./scriptsPg/1.9.0";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
@@ -16,7 +17,8 @@ import m3 from "./scriptsPg/1.8.0";
|
||||
const migrations = [
|
||||
{ version: "1.6.0", run: m1 },
|
||||
{ version: "1.7.0", run: m2 },
|
||||
{ version: "1.8.0", run: m3 }
|
||||
{ version: "1.8.0", run: m3 },
|
||||
{ version: "1.9.0", run: m4 }
|
||||
// Add new migrations here as they are created
|
||||
] as {
|
||||
version: string;
|
||||
|
||||
@@ -25,6 +25,7 @@ import m20 from "./scriptsSqlite/1.5.0";
|
||||
import m21 from "./scriptsSqlite/1.6.0";
|
||||
import m22 from "./scriptsSqlite/1.7.0";
|
||||
import m23 from "./scriptsSqlite/1.8.0";
|
||||
import m24 from "./scriptsSqlite/1.9.0";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
@@ -49,6 +50,7 @@ const migrations = [
|
||||
{ version: "1.6.0", run: m21 },
|
||||
{ version: "1.7.0", run: m22 },
|
||||
{ version: "1.8.0", run: m23 },
|
||||
{ version: "1.9.0", run: m24 },
|
||||
// Add new migrations here as they are created
|
||||
] as const;
|
||||
|
||||
|
||||
25
server/setup/scriptsPg/1.9.0.ts
Normal file
25
server/setup/scriptsPg/1.9.0.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { db } from "@server/db/pg/driver";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
const version = "1.9.0";
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
try {
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "setupTokens" (
|
||||
"tokenId" varchar PRIMARY KEY NOT NULL,
|
||||
"token" varchar NOT NULL,
|
||||
"used" boolean DEFAULT false NOT NULL,
|
||||
"dateCreated" varchar NOT NULL,
|
||||
"dateUsed" varchar
|
||||
);
|
||||
`);
|
||||
|
||||
console.log(`Added setupTokens table`);
|
||||
} catch (e) {
|
||||
console.log("Unable to add setupTokens table:", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
35
server/setup/scriptsSqlite/1.9.0.ts
Normal file
35
server/setup/scriptsSqlite/1.9.0.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { APP_PATH } from "@server/lib/consts";
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
|
||||
const version = "1.9.0";
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||
const db = new Database(location);
|
||||
|
||||
try {
|
||||
db.pragma("foreign_keys = OFF");
|
||||
|
||||
db.transaction(() => {
|
||||
db.exec(`
|
||||
CREATE TABLE 'setupTokens' (
|
||||
'tokenId' text PRIMARY KEY NOT NULL,
|
||||
'token' text NOT NULL,
|
||||
'used' integer DEFAULT 0 NOT NULL,
|
||||
'dateCreated' text NOT NULL,
|
||||
'dateUsed' text
|
||||
);
|
||||
`);
|
||||
})();
|
||||
|
||||
db.pragma("foreign_keys = ON");
|
||||
|
||||
console.log(`Added setupTokens table`);
|
||||
} catch (e) {
|
||||
console.log("Unable to add setupTokens table:", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user