mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-01 17:44:52 +02:00
add set server admin password to cli
This commit is contained in:
parent
ddd292422b
commit
fc19d0ba8b
9 changed files with 172 additions and 5 deletions
|
@ -26,3 +26,4 @@ install/
|
||||||
bruno/
|
bruno/
|
||||||
LICENSE
|
LICENSE
|
||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
|
dist
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
141
cli/commands/setAdminCredentials.ts
Normal file
141
cli/commands/setAdminCredentials.ts
Normal 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
11
cli/index.ts
Normal 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
3
cli/wrapper.sh
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
cd /app/
|
||||||
|
./dist/cli.mjs "$@"
|
|
@ -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": {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"@server/*": ["../server/*"],
|
"@server/*": ["../server/*"],
|
||||||
"@test/*": ["../test/*"],
|
"@test/*": ["../test/*"],
|
||||||
"@app/*": ["*"],
|
"@app/*": ["*"],
|
||||||
|
"@cli/*": ["../cli/*"],
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue