add set server admin password to cli

This commit is contained in:
miloschwartz 2025-06-15 13:19:07 -04:00
parent ddd292422b
commit fc19d0ba8b
No known key found for this signature in database
9 changed files with 172 additions and 5 deletions

View file

@ -26,3 +26,4 @@ install/
bruno/ bruno/
LICENSE LICENSE
CONTRIBUTING.md CONTRIBUTING.md
dist

View file

@ -13,6 +13,7 @@ RUN echo 'export * from "./sqlite";' > server/db/index.ts
RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema.ts --out init RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema.ts --out init
RUN npm run build:sqlite RUN npm run build:sqlite
RUN npm run build:cli
FROM node:20-alpine AS runner FROM node:20-alpine AS runner
@ -30,6 +31,9 @@ COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
COPY --from=builder /app/init ./dist/init COPY --from=builder /app/init ./dist/init
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
COPY server/db/names.json ./dist/names.json COPY server/db/names.json ./dist/names.json
COPY public ./public COPY public ./public

View file

@ -13,6 +13,7 @@ RUN echo 'export * from "./pg";' > server/db/index.ts
RUN npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init RUN npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init
RUN npm run build:pg RUN npm run build:pg
RUN npm run build:cli
FROM node:20-alpine AS runner FROM node:20-alpine AS runner
@ -30,6 +31,9 @@ COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
COPY --from=builder /app/init ./dist/init COPY --from=builder /app/init ./dist/init
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
COPY server/db/names.json ./dist/names.json COPY server/db/names.json ./dist/names.json
COPY public ./public COPY public ./public

View file

@ -0,0 +1,141 @@
import { CommandModule } from "yargs";
import { hashPassword, verifyPassword } from "@server/auth/password";
import { db, resourceSessions, sessions } from "@server/db";
import { users } from "@server/db";
import { eq, inArray } from "drizzle-orm";
import moment from "moment";
import { fromError } from "zod-validation-error";
import { passwordSchema } from "@server/auth/passwordSchema";
import { UserType } from "@server/types/UserTypes";
import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
type SetAdminCredentialsArgs = {
email: string;
password: string;
};
export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = {
command: "set-admin-credentials",
describe: "Set the server admin credentials",
builder: (yargs) => {
return yargs
.option("email", {
type: "string",
demandOption: true,
describe: "Admin email address"
})
.option("password", {
type: "string",
demandOption: true,
describe: "Admin password"
});
},
handler: async (argv: { email: string; password: string }) => {
try {
const { email, password } = argv;
const parsed = passwordSchema.safeParse(password);
if (!parsed.success) {
throw Error(
`Invalid server admin password: ${fromError(parsed.error).toString()}`
);
}
const passwordHash = await hashPassword(password);
await db.transaction(async (trx) => {
try {
const [existing] = await trx
.select()
.from(users)
.where(eq(users.serverAdmin, true));
if (existing) {
const passwordChanged = !(await verifyPassword(
password,
existing.passwordHash!
));
if (passwordChanged) {
await trx
.update(users)
.set({ passwordHash })
.where(eq(users.userId, existing.userId));
await invalidateAllSessions(existing.userId);
console.log("Server admin password updated");
}
if (existing.email !== email) {
await trx
.update(users)
.set({ email, username: email })
.where(eq(users.userId, existing.userId));
console.log("Server admin email updated");
}
} else {
const userId = generateId(15);
await trx.update(users).set({ serverAdmin: false });
await db.insert(users).values({
userId: userId,
email: email,
type: UserType.Internal,
username: email,
passwordHash,
dateCreated: moment().toISOString(),
serverAdmin: true,
emailVerified: true
});
console.log("Server admin created");
}
} catch (e) {
console.error("Failed to set admin credentials", e);
trx.rollback();
throw e;
}
});
console.log("Admin credentials updated successfully");
process.exit(0);
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
};
export async function invalidateAllSessions(userId: string): Promise<void> {
try {
await db.transaction(async (trx) => {
const userSessions = await trx
.select()
.from(sessions)
.where(eq(sessions.userId, userId));
await trx.delete(resourceSessions).where(
inArray(
resourceSessions.userSessionId,
userSessions.map((s) => s.sessionId)
)
);
await trx.delete(sessions).where(eq(sessions.userId, userId));
});
} catch (e) {
console.log("Failed to all invalidate user sessions", e);
}
}
const random: RandomReader = {
read(bytes: Uint8Array): void {
crypto.getRandomValues(bytes);
}
};
export function generateId(length: number): string {
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
return generateRandomString(random, alphabet, length);
}

11
cli/index.ts Normal file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env node
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
yargs(hideBin(process.argv))
.scriptName("pangctl")
.command(setAdminCredentials)
.demandCommand()
.help().argv;

3
cli/wrapper.sh Normal file
View file

@ -0,0 +1,3 @@
#!/bin/sh
cd /app/
./dist/cli.mjs "$@"

View file

@ -23,7 +23,8 @@
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs", "build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
"start:sqlite": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'", "start:sqlite": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
"start:pg": "DB_TYPE=pg NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'", "start:pg": "DB_TYPE=pg NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
"email": "email dev --dir server/emails/templates --port 3005" "email": "email dev --dir server/emails/templates --port 3005",
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.2", "@asteasolutions/zod-to-openapi": "^7.3.2",
@ -106,7 +107,8 @@
"winston-daily-rotate-file": "5.0.0", "winston-daily-rotate-file": "5.0.0",
"ws": "8.18.2", "ws": "8.18.2",
"zod": "3.25.56", "zod": "3.25.56",
"zod-validation-error": "3.4.1" "zod-validation-error": "3.4.1",
"yargs": "18.0.0"
}, },
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "1.44.1", "@dotenvx/dotenvx": "1.44.1",
@ -137,8 +139,7 @@
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
"tsx": "4.19.4", "tsx": "4.19.4",
"typescript": "^5", "typescript": "^5",
"typescript-eslint": "^8.34.0", "typescript-eslint": "^8.34.0"
"yargs": "18.0.0"
}, },
"overrides": { "overrides": {
"emblor": { "emblor": {

View file

@ -53,7 +53,7 @@ export async function setupServerAdmin() {
if (existing.email !== email) { if (existing.email !== email) {
await trx await trx
.update(users) .update(users)
.set({ email }) .set({ email, username: email })
.where(eq(users.userId, existing.userId)); .where(eq(users.userId, existing.userId));
logger.info(`Server admin email updated`); logger.info(`Server admin email updated`);
@ -77,6 +77,7 @@ export async function setupServerAdmin() {
logger.info(`Server admin created`); logger.info(`Server admin created`);
} }
} catch (e) { } catch (e) {
console.error("Failed to setup server admin");
logger.error(e); logger.error(e);
trx.rollback(); trx.rollback();
} }

View file

@ -17,6 +17,7 @@
"@server/*": ["../server/*"], "@server/*": ["../server/*"],
"@test/*": ["../test/*"], "@test/*": ["../test/*"],
"@app/*": ["*"], "@app/*": ["*"],
"@cli/*": ["../cli/*"],
"@/*": ["./*"] "@/*": ["./*"]
}, },
"plugins": [ "plugins": [