cli: user: add token generation (#140)

* cli: user: add token generation

* cli: user: add token generation

* cli: user: check for value being cancel before continuing
This commit is contained in:
dswbx
2025-04-05 17:57:03 +02:00
committed by GitHub
parent de984fa101
commit ca86fa58ac
2 changed files with 65 additions and 36 deletions

View File

@@ -1,4 +1,4 @@
import { type DB, Exception } from "core"; import { type DB, Exception, type PrimaryFieldType } from "core";
import { addFlashMessage } from "core/server/flash"; import { addFlashMessage } from "core/server/flash";
import { import {
type Static, type Static,
@@ -14,6 +14,7 @@ import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
import { sign, verify } from "hono/jwt"; import { sign, verify } from "hono/jwt";
import type { CookieOptions } from "hono/utils/cookie"; import type { CookieOptions } from "hono/utils/cookie";
import type { ServerEnv } from "modules/Controller"; import type { ServerEnv } from "modules/Controller";
import { pick } from "lodash-es";
type Input = any; // workaround type Input = any; // workaround
export type JWTPayload = Parameters<typeof sign>[0]; export type JWTPayload = Parameters<typeof sign>[0];
@@ -37,11 +38,10 @@ export interface Strategy {
} }
export type User = { export type User = {
id: number; id: PrimaryFieldType;
email: string; email: string;
username: string;
password: string; password: string;
role: string; role?: string | null;
}; };
export type ProfileExchange = { export type ProfileExchange = {
@@ -158,13 +158,8 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
} }
// @todo: add jwt tests // @todo: add jwt tests
async jwt(user: Omit<User, "password">): Promise<string> { async jwt(_user: Omit<User, "password">): Promise<string> {
const prohibited = ["password"]; const user = pick(_user, this.config.jwt.fields);
for (const prop of prohibited) {
if (prop in user) {
throw new Error(`Property "${prop}" is prohibited`);
}
}
const payload: JWTPayload = { const payload: JWTPayload = {
...user, ...user,

View File

@@ -1,20 +1,29 @@
import { password as $password, text as $text } from "@clack/prompts"; import {
password as $password,
text as $text,
log as $log,
isCancel as $isCancel,
} from "@clack/prompts";
import type { App } from "App"; import type { App } from "App";
import type { PasswordStrategy } from "auth/authenticate/strategies"; import type { PasswordStrategy } from "auth/authenticate/strategies";
import { makeConfigApp } from "cli/commands/run"; import { makeConfigApp } from "cli/commands/run";
import { getConfigPath } from "cli/commands/run/platform"; import { getConfigPath } from "cli/commands/run/platform";
import type { CliBkndConfig, CliCommand } from "cli/types"; import type { CliBkndConfig, CliCommand } from "cli/types";
import { Argument } from "commander"; import { Argument } from "commander";
import { $console } from "core";
import c from "picocolors";
export const user: CliCommand = (program) => { export const user: CliCommand = (program) => {
program program
.command("user") .command("user")
.description("create and update user (auth)") .description("create/update users, or generate a token (auth)")
.addArgument(new Argument("<action>", "action to perform").choices(["create", "update"])) .addArgument(
new Argument("<action>", "action to perform").choices(["create", "update", "token"]),
)
.action(action); .action(action);
}; };
async function action(action: "create" | "update", options: any) { async function action(action: "create" | "update" | "token", options: any) {
const configFilePath = await getConfigPath(); const configFilePath = await getConfigPath();
if (!configFilePath) { if (!configFilePath) {
console.error("config file not found"); console.error("config file not found");
@@ -31,6 +40,9 @@ async function action(action: "create" | "update", options: any) {
case "update": case "update":
await update(app, options); await update(app, options);
break; break;
case "token":
await token(app, options);
break;
} }
} }
@@ -38,7 +50,8 @@ async function create(app: App, options: any) {
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy; const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
if (!strategy) { if (!strategy) {
throw new Error("Password strategy not configured"); $log.error("Password strategy not configured");
process.exit(1);
} }
const email = await $text({ const email = await $text({
@@ -50,6 +63,7 @@ async function create(app: App, options: any) {
return; return;
}, },
}); });
if ($isCancel(email)) process.exit(1);
const password = await $password({ const password = await $password({
message: "Enter password", message: "Enter password",
@@ -60,20 +74,17 @@ async function create(app: App, options: any) {
return; return;
}, },
}); });
if ($isCancel(password)) process.exit(1);
if (typeof email !== "string" || typeof password !== "string") {
console.log("Cancelled");
process.exit(0);
}
try { try {
const created = await app.createUser({ const created = await app.createUser({
email, email,
password: await strategy.hash(password as string), password: await strategy.hash(password as string),
}); });
console.log("Created:", created); $log.success(`Created user: ${c.cyan(created.email)}`);
} catch (e) { } catch (e) {
console.error("Error", e); $log.error("Error creating user");
$console.error(e);
} }
} }
@@ -92,17 +103,14 @@ async function update(app: App, options: any) {
return; return;
}, },
})) as string; })) as string;
if (typeof email !== "string") { if ($isCancel(email)) process.exit(1);
console.log("Cancelled");
process.exit(0);
}
const { data: user } = await em.repository(users_entity).findOne({ email }); const { data: user } = await em.repository(users_entity).findOne({ email });
if (!user) { if (!user) {
console.log("User not found"); $log.error("User not found");
process.exit(0); process.exit(1);
} }
console.log("User found:", user); $log.info(`User found: ${c.cyan(user.email)}`);
const password = await $password({ const password = await $password({
message: "New Password?", message: "New Password?",
@@ -113,10 +121,7 @@ async function update(app: App, options: any) {
return; return;
}, },
}); });
if (typeof password !== "string") { if ($isCancel(password)) process.exit(1);
console.log("Cancelled");
process.exit(0);
}
try { try {
function togglePw(visible: boolean) { function togglePw(visible: boolean) {
@@ -134,8 +139,37 @@ async function update(app: App, options: any) {
}); });
togglePw(false); togglePw(false);
console.log("Updated:", user); $log.success(`Updated user: ${c.cyan(user.email)}`);
} catch (e) { } catch (e) {
console.error("Error", e); $log.error("Error updating user");
$console.error(e);
} }
} }
async function token(app: App, options: any) {
const config = app.module.auth.toJSON(true);
const users_entity = config.entity_name as "users";
const em = app.modules.ctx().em;
const email = (await $text({
message: "Which user? Enter email",
validate: (v) => {
if (!v.includes("@")) {
return "Invalid email";
}
return;
},
})) as string;
if ($isCancel(email)) process.exit(1);
const { data: user } = await em.repository(users_entity).findOne({ email });
if (!user) {
$log.error("User not found");
process.exit(1);
}
$log.info(`User found: ${c.cyan(user.email)}`);
console.log(
`\n${c.dim("Token:")}\n${c.yellow(await app.module.auth.authenticator.jwt(user))}\n`,
);
}