From ca86fa58ac7d84159066c427cccd1c8dcfec2a6f Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 5 Apr 2025 17:57:03 +0200 Subject: [PATCH] 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 --- app/src/auth/authenticate/Authenticator.ts | 17 ++--- app/src/cli/commands/user.ts | 84 +++++++++++++++------- 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 6d1acc5..78c2db7 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -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 { type Static, @@ -14,6 +14,7 @@ import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; import { sign, verify } from "hono/jwt"; import type { CookieOptions } from "hono/utils/cookie"; import type { ServerEnv } from "modules/Controller"; +import { pick } from "lodash-es"; type Input = any; // workaround export type JWTPayload = Parameters[0]; @@ -37,11 +38,10 @@ export interface Strategy { } export type User = { - id: number; + id: PrimaryFieldType; email: string; - username: string; password: string; - role: string; + role?: string | null; }; export type ProfileExchange = { @@ -158,13 +158,8 @@ export class Authenticator = Record< } // @todo: add jwt tests - async jwt(user: Omit): Promise { - const prohibited = ["password"]; - for (const prop of prohibited) { - if (prop in user) { - throw new Error(`Property "${prop}" is prohibited`); - } - } + async jwt(_user: Omit): Promise { + const user = pick(_user, this.config.jwt.fields); const payload: JWTPayload = { ...user, diff --git a/app/src/cli/commands/user.ts b/app/src/cli/commands/user.ts index 3545093..77152a4 100644 --- a/app/src/cli/commands/user.ts +++ b/app/src/cli/commands/user.ts @@ -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 { PasswordStrategy } from "auth/authenticate/strategies"; import { makeConfigApp } from "cli/commands/run"; import { getConfigPath } from "cli/commands/run/platform"; import type { CliBkndConfig, CliCommand } from "cli/types"; import { Argument } from "commander"; +import { $console } from "core"; +import c from "picocolors"; export const user: CliCommand = (program) => { program .command("user") - .description("create and update user (auth)") - .addArgument(new Argument("", "action to perform").choices(["create", "update"])) + .description("create/update users, or generate a token (auth)") + .addArgument( + new Argument("", "action to perform").choices(["create", "update", "token"]), + ) .action(action); }; -async function action(action: "create" | "update", options: any) { +async function action(action: "create" | "update" | "token", options: any) { const configFilePath = await getConfigPath(); if (!configFilePath) { console.error("config file not found"); @@ -31,6 +40,9 @@ async function action(action: "create" | "update", options: any) { case "update": await update(app, options); 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; if (!strategy) { - throw new Error("Password strategy not configured"); + $log.error("Password strategy not configured"); + process.exit(1); } const email = await $text({ @@ -50,6 +63,7 @@ async function create(app: App, options: any) { return; }, }); + if ($isCancel(email)) process.exit(1); const password = await $password({ message: "Enter password", @@ -60,20 +74,17 @@ async function create(app: App, options: any) { return; }, }); - - if (typeof email !== "string" || typeof password !== "string") { - console.log("Cancelled"); - process.exit(0); - } + if ($isCancel(password)) process.exit(1); try { const created = await app.createUser({ email, password: await strategy.hash(password as string), }); - console.log("Created:", created); + $log.success(`Created user: ${c.cyan(created.email)}`); } 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; }, })) as string; - if (typeof email !== "string") { - console.log("Cancelled"); - process.exit(0); - } + if ($isCancel(email)) process.exit(1); const { data: user } = await em.repository(users_entity).findOne({ email }); if (!user) { - console.log("User not found"); - process.exit(0); + $log.error("User not found"); + process.exit(1); } - console.log("User found:", user); + $log.info(`User found: ${c.cyan(user.email)}`); const password = await $password({ message: "New Password?", @@ -113,10 +121,7 @@ async function update(app: App, options: any) { return; }, }); - if (typeof password !== "string") { - console.log("Cancelled"); - process.exit(0); - } + if ($isCancel(password)) process.exit(1); try { function togglePw(visible: boolean) { @@ -134,8 +139,37 @@ async function update(app: App, options: any) { }); togglePw(false); - console.log("Updated:", user); + $log.success(`Updated user: ${c.cyan(user.email)}`); } 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`, + ); +}