diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2edd928c..34620b14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,6 +156,17 @@ jobs: pnpm exec nuxi prepare pnpm run lint + # The scaffold's own `eslint --fix` pass hides a template that breaks the + # project's style, but `pull` merges the raw template into the user's + # formatted copy, so every such difference becomes a conflict. Pages and + # layouts sit outside the paths that exempt them from multi-word names here. + - name: Lint the raw templates with the project's config + working-directory: ${{ runner.temp }}/scaffold/ci-app + run: | + cp -r "$GITHUB_WORKSPACE/packages/preset-nuxt4/templates" __templates + pnpm exec eslint --rule '{"vue/multi-word-component-names":"off"}' __templates + rm -rf __templates + - name: Typecheck working-directory: ${{ runner.temp }}/scaffold/ci-app run: pnpm run typecheck diff --git a/CHANGELOG.md b/CHANGELOG.md index b132b8a2..13ebc844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,13 @@ - Pin generated projects to TypeScript 6: `vue-tsc` and typescript-eslint do not support TypeScript 7 yet. A feature's explicit dependency range now also wins over a peer pin that `nuxi module add` wrote into the other bucket. +- Install the lefthook git hooks: generated projects get a + `prepare: lefthook install || exit 0` script, and `pull` runs it once. +- Format the template files with the generated project's own ESLint config, so + `pull` no longer turns style differences into merge conflicts. CI now lints + the raw templates with that config. +- `battlestack --help` prints that command's usage, and `--help` + inside a project lists the core commands (`pull`, `own`, `doctor`, …). ## v1.3.0 (2026-09-24) diff --git a/packages/cli/src/cli/help.ts b/packages/cli/src/cli/help.ts index e15e43a4..c5e3738b 100644 --- a/packages/cli/src/cli/help.ts +++ b/packages/cli/src/cli/help.ts @@ -17,6 +17,30 @@ export async function printHelp( } } +/** Dispatched in `index.ts` before project detection, so it is not a reserved project command. */ +const SELF_UPDATE_HELP = { + name: 'self-update', + usage: 'battlestack self-update [--force]', + label: 'upgrade the globally-installed CLI to the latest release', + helpExtra: [['battlestack self-update --force', 'opt out of pnpm release-age gate; install true latest now']] as [string, string][], +} + +/** `battlestack --help` for a reserved command or `self-update`. False when `name` is neither. */ +export function printCommandHelp(name: string | undefined): boolean { + const cmd = [...RESERVED_COMMANDS, SELF_UPDATE_HELP].find((c) => c.name === name) + if (!cmd) return false + ui.plain(pc.bold('Usage')) + ui.dim(` ${cmd.usage}`) + ui.blank() + ui.plain(` ${cmd.label}`) + if (cmd.helpExtra) { + ui.blank() + ui.kv(cmd.helpExtra, ' ') + } + ui.blank() + return true +} + /** * Plugin-contributed commands, one block per owning plugin. Scaffold-only ids * are excluded, plus anything in `claimed` that would lose dispatch anyway. diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f86197ea..710095ab 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -18,7 +18,7 @@ import { import { installUiPort, ui } from '@battlestack/tui' import { parseArgs, stripCommandToken } from './cli/args.js' import { dispatchPluginCommand } from './cli/plugin-commands.js' -import { printHelp } from './cli/help.js' +import { printCommandHelp, printHelp } from './cli/help.js' import { bootstrapProject } from './commands/install.js' import { doctorCommand } from './commands/doctor.js' import { gatewayUp, registerProject } from './commands/gateway.js' @@ -149,6 +149,10 @@ async function main(): Promise { // Runs inside a project or not: it upgrades the globally-installed CLI, not deps. if (args.projectName === 'self-update' || args.projectName === 'update') { + if (args.help) { + printCommandHelp('self-update') + return + } try { await selfUpdateCommand({ packageManager: args.packageManager, @@ -247,13 +251,9 @@ async function main(): Promise { } if (args.help) { - if (!projectRoot) { - ui.banner(VERSION) - await printHelp('scaffold', registries) - return - } - // Inside a project, `--help` lists rather than executes. - await projectCommand({ ...args, projectName: undefined }, loader, projectRoot, registries, rawArgv) + if (printCommandHelp(args.projectName)) return + ui.banner(VERSION) + await printHelp(projectRoot ? 'project' : 'scaffold', registries, projectRoot ?? undefined) return } diff --git a/packages/cli/test/help.test.ts b/packages/cli/test/help.test.ts index 2c78b3fc..c910b7e2 100644 --- a/packages/cli/test/help.test.ts +++ b/packages/cli/test/help.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { BattlestackRegistries } from '@battlestack/core' -import { printHelp } from '../src/cli/help.js' +import { printCommandHelp, printHelp } from '../src/cli/help.js' afterEach(() => { vi.restoreAllMocks() @@ -50,4 +50,23 @@ describe('printHelp', () => { it('defaults to scaffold mode', async () => { expect(await capture()).toContain('--template') }) + + it('prints a reserved command\'s own usage for ` --help`', () => { + const lines: string[] = [] + vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => { + lines.push(args.join(' ')) + }) + expect(printCommandHelp('own')).toBe(true) + expect(lines.join('\n')).toContain('battlestack own ') + expect(printCommandHelp('my-app')).toBe(false) + }) + + it('prints self-update\'s usage, which is dispatched before the reserved commands', () => { + const lines: string[] = [] + vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => { + lines.push(args.join(' ')) + }) + expect(printCommandHelp('self-update')).toBe(true) + expect(lines.join('\n')).toContain('battlestack self-update [--force]') + }) }) diff --git a/packages/preset-nuxt4/src/features/audit-log.ts b/packages/preset-nuxt4/src/features/audit-log.ts index 68766147..651a4cb0 100644 --- a/packages/preset-nuxt4/src/features/audit-log.ts +++ b/packages/preset-nuxt4/src/features/audit-log.ts @@ -6,7 +6,7 @@ import { STAGE } from '@battlestack/core' /** Append-only audit trail. Best-effort on insert. */ export const auditLogFeature: Feature = { id: 'nuxt4:audit-log', - version: '1.0.1', + version: '1.0.2', label: 'Audit log (security events)', description: 'Append-only trail of login, signup, role changes, and passkey events.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/auth-2fa.ts b/packages/preset-nuxt4/src/features/auth-2fa.ts index ca28bd2a..0c69ce02 100644 --- a/packages/preset-nuxt4/src/features/auth-2fa.ts +++ b/packages/preset-nuxt4/src/features/auth-2fa.ts @@ -7,7 +7,7 @@ import { STAGE } from '@battlestack/core' /** TOTP-based two-factor auth. Secrets encrypted at rest with AES-256-GCM via `NUXT_TOTP_ENCRYPTION_KEY`. */ export const auth2faFeature: Feature = { id: 'nuxt4:auth-2fa', - version: '1.2.2', + version: '1.2.3', label: 'Two-factor auth (TOTP)', description: 'Authenticator-app 2FA; TOTP secrets encrypted at rest.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/auth-recovery.ts b/packages/preset-nuxt4/src/features/auth-recovery.ts index 747ca365..e9ea9ba8 100644 --- a/packages/preset-nuxt4/src/features/auth-recovery.ts +++ b/packages/preset-nuxt4/src/features/auth-recovery.ts @@ -6,7 +6,7 @@ import { STAGE } from '@battlestack/core' /** Forgot-password and reset-password. Tokens sha256-hashed at rest. */ export const authRecoveryFeature: Feature = { id: 'nuxt4:auth-recovery', - version: '1.1.2', + version: '1.1.3', label: 'Password recovery', description: 'Forgot-password and reset flows with one-time, hashed tokens.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/auth-verification.ts b/packages/preset-nuxt4/src/features/auth-verification.ts index 0c42a4fb..5f9588c2 100644 --- a/packages/preset-nuxt4/src/features/auth-verification.ts +++ b/packages/preset-nuxt4/src/features/auth-verification.ts @@ -7,7 +7,7 @@ import { STAGE } from '@battlestack/core' /** Email verification: a signup token verified via a one-time link, optionally blocking login. */ export const authVerificationFeature: Feature = { id: 'nuxt4:auth-verification', - version: '1.0.2', + version: '1.0.3', label: 'Email verification', frameworks: ['nuxt4'], stage: STAGE.AUTH_EXTRAS, diff --git a/packages/preset-nuxt4/src/features/auth.ts b/packages/preset-nuxt4/src/features/auth.ts index 91f5de50..558d75f3 100644 --- a/packages/preset-nuxt4/src/features/auth.ts +++ b/packages/preset-nuxt4/src/features/auth.ts @@ -30,7 +30,7 @@ function titleCase(slug: string): string { /** Session auth on `nuxt-auth-utils`. The sealed cookie carries an opaque sessionId. */ export const authFeature: Feature = { id: 'nuxt4:auth', - version: '1.10.1', + version: '1.10.2', label: 'Session-based auth (argon2id)', frameworks: ['nuxt4'], stage: STAGE.AUTH, diff --git a/packages/preset-nuxt4/src/features/build-info.ts b/packages/preset-nuxt4/src/features/build-info.ts index 96d7e673..14a5482b 100644 --- a/packages/preset-nuxt4/src/features/build-info.ts +++ b/packages/preset-nuxt4/src/features/build-info.ts @@ -12,7 +12,7 @@ import { patchNuxtConfig } from '../utils/nuxt-config.js' */ export const buildInfoFeature: Feature = { id: 'nuxt4:build-info', - version: '1.0.0', + version: '1.0.1', label: 'Build info (version + commit in the footer)', description: 'Version and commit sha in the app footer, from the image\'s build args.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/chat.ts b/packages/preset-nuxt4/src/features/chat.ts index 92cb27af..43d34a1c 100644 --- a/packages/preset-nuxt4/src/features/chat.ts +++ b/packages/preset-nuxt4/src/features/chat.ts @@ -5,7 +5,7 @@ import { saveFeatureState, STAGE, type Feature, type ChatTransport } from '@batt /** Streaming chat on Mastra's default agent. Transport from `state.chatTransport`. */ export const chatFeature: Feature = { id: 'nuxt4:chat', - version: '1.3.0', + version: '1.3.1', label: 'Streaming chat (Mastra)', description: 'WebSocket streaming chat UI backed by the Mastra default agent.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/ci.ts b/packages/preset-nuxt4/src/features/ci.ts index 0bf57550..de97dd9d 100644 --- a/packages/preset-nuxt4/src/features/ci.ts +++ b/packages/preset-nuxt4/src/features/ci.ts @@ -7,17 +7,23 @@ import { type RunContext, type PackageManager, } from '@battlestack/core' -import { writeFileEnsured } from '@battlestack/core/utils/fs.js' +import { readJson, writeFileEnsured, writeJson } from '@battlestack/core/utils/fs.js' +import { run } from '@battlestack/core/utils/run.js' import { templatesDir } from '@battlestack/core/utils/templates.js' import { applyVars, renderPmVars } from '../utils/pm-template.js' import { STAGE } from '@battlestack/core/constants/stages.js' const FILES = ['lefthook.yml'] as const +// lefthook's own postinstall runs at scaffold time, before the project has a `.git`. +// `|| exit 0` keeps installs without a `.git` (Docker) or without dev deps from failing. +const PREPARE = 'lefthook install || exit 0' +const PREPARE_TAKEN = 'package.json has its own `prepare` script, so `lefthook install` was not added; make sure it runs it.' + /** Lefthook git hooks: pre-commit eslint. Typecheck runs in CI, not on push. */ export const ciFeature: Feature = { id: 'shared:ci', - version: '1.1.1', + version: '1.1.2', label: 'Git hooks (lefthook)', description: 'Pre-commit eslint on staged files via lefthook.', stage: STAGE.GITIGNORE, @@ -43,10 +49,17 @@ export const ciFeature: Feature = { async execute(ctx) { await emit(ctx) + await wirePrepare(ctx) }, async update(ctx, _prev) { await emit(ctx) + if (!await wirePrepare(ctx)) { + return { written: [...FILES], skipped: [], notes: [PREPARE_TAKEN] } + } + // pull's install takes pnpm's up-to-date fast path, which skips `prepare`. + const pm = String(ctx.state.packageManager ?? 'pnpm') as PackageManager + await run(pm, ['run', 'prepare'], { cwd: ctx.projectDir, inherit: ctx.debug }).catch(() => {}) return { written: [...FILES], skipped: [], notes: [] } }, } @@ -64,3 +77,15 @@ async function emit(ctx: RunContext): Promise { recordFile(ctx, 'shared:ci', rel, await hashFile(dest)) } } + +/** Adds the `prepare` hook installer. False when the project's own `prepare` is in the way. */ +async function wirePrepare(ctx: RunContext): Promise { + const pkgPath = path.join(ctx.projectDir, 'package.json') + const pkg = await readJson>(pkgPath) + const scripts = (pkg.scripts as Record | undefined) ?? {} + if (scripts.prepare) return scripts.prepare === PREPARE + scripts.prepare = PREPARE + pkg.scripts = scripts + await writeJson(pkgPath, pkg) + return true +} diff --git a/packages/preset-nuxt4/src/features/dashboard-shell.ts b/packages/preset-nuxt4/src/features/dashboard-shell.ts index 482c80af..557684e4 100644 --- a/packages/preset-nuxt4/src/features/dashboard-shell.ts +++ b/packages/preset-nuxt4/src/features/dashboard-shell.ts @@ -5,7 +5,7 @@ import { patchNuxtConfig } from '../utils/nuxt-config.js' /** Authenticated app shell under `/dashboard/*`. */ export const dashboardShellFeature: Feature = { id: 'nuxt4:dashboard-shell', - version: '1.3.1', + version: '1.3.2', label: 'Authenticated app shell (/dashboard/*)', frameworks: ['nuxt4'], stage: STAGE.AUTH, diff --git a/packages/preset-nuxt4/src/features/database.ts b/packages/preset-nuxt4/src/features/database.ts index a7c1c35f..cb8be16d 100644 --- a/packages/preset-nuxt4/src/features/database.ts +++ b/packages/preset-nuxt4/src/features/database.ts @@ -251,7 +251,7 @@ async function registerRuntimeConfig(projectDir: string): Promise { /** PostgreSQL 18 in Docker, Drizzle ORM, users schema, seed script. */ export const databaseFeature: Feature = { id: 'nuxt4:database', - version: '1.7.1', + version: '1.7.2', label: 'PostgreSQL + Drizzle ORM (Docker)', frameworks: ['nuxt4'], stage: STAGE.DATABASE, diff --git a/packages/preset-nuxt4/src/features/essentials.ts b/packages/preset-nuxt4/src/features/essentials.ts index a0f7f14d..a614a0fd 100644 --- a/packages/preset-nuxt4/src/features/essentials.ts +++ b/packages/preset-nuxt4/src/features/essentials.ts @@ -16,7 +16,7 @@ const ICON_FILES = [ /** Always-on Nuxt essentials: `@nuxt/eslint`, `@nuxt/fonts`, `@nuxt/image`, datepicker, `@iconify/vue`. */ export const essentialsFeature: Feature = { id: 'nuxt4:essentials', - version: '1.2.1', + version: '1.2.2', label: 'Nuxt essentials (eslint, fonts, image, iconify, datepicker, nodemailer)', frameworks: ['nuxt4'], stage: STAGE.BASE_CONFIG, diff --git a/packages/preset-nuxt4/src/features/health.ts b/packages/preset-nuxt4/src/features/health.ts index c602572a..d5406e9d 100644 --- a/packages/preset-nuxt4/src/features/health.ts +++ b/packages/preset-nuxt4/src/features/health.ts @@ -5,7 +5,7 @@ import { patchNuxtConfig } from '../utils/nuxt-config.js' /** `/api/health` + probe split (`/live`, `/ready`) + runtimeConfig knobs. Picks `with-db` or `env-only` variant at scaffold time. */ export const healthFeature: Feature = { id: 'nuxt4:health', - version: '1.5.0', + version: '1.5.1', label: 'Health endpoints (/api/health, /live, /ready)', frameworks: ['nuxt4'], stage: STAGE.BASE_CONFIG, diff --git a/packages/preset-nuxt4/src/features/i18n.ts b/packages/preset-nuxt4/src/features/i18n.ts index b80d889e..3274c93a 100644 --- a/packages/preset-nuxt4/src/features/i18n.ts +++ b/packages/preset-nuxt4/src/features/i18n.ts @@ -6,7 +6,7 @@ import { patchNuxtConfig } from '../utils/nuxt-config.js' /** @nuxtjs/i18n with per-locale namespace JSONs under `i18n/locales//.json`. */ export const i18nFeature: Feature = { id: 'nuxt4:i18n', - version: '1.1.1', + version: '1.1.2', label: 'i18n (Dutch + English)', frameworks: ['nuxt4'], stage: STAGE.I18N, diff --git a/packages/preset-nuxt4/src/features/landing-shell.ts b/packages/preset-nuxt4/src/features/landing-shell.ts index a190345d..86f6eb65 100644 --- a/packages/preset-nuxt4/src/features/landing-shell.ts +++ b/packages/preset-nuxt4/src/features/landing-shell.ts @@ -7,7 +7,7 @@ import { applyColorAliases } from './battlestack-theme.js' /** Frontend shell: `app.vue`, `app.config.ts`, layouts, landing page. All emitted files are structural. */ export const landingShellFeature: Feature = { id: 'nuxt4:landing-shell', - version: '1.4.0', + version: '1.4.1', label: 'Landing shell (layouts, public landing page)', description: 'Public landing page, layouts, and frontend app shell.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/mastra.ts b/packages/preset-nuxt4/src/features/mastra.ts index 687fa03d..b105ccae 100644 --- a/packages/preset-nuxt4/src/features/mastra.ts +++ b/packages/preset-nuxt4/src/features/mastra.ts @@ -29,7 +29,7 @@ import { /** Mastra AI runtime. Talks to an OpenAI-compatible AI gateway (sluis.ai preset, or any compatible URL) via `@ai-sdk/openai-compatible`. */ export const mastraFeature: Feature = { id: 'nuxt4:mastra', - version: '2.1.3', + version: '2.1.4', label: 'Mastra AI runtime', frameworks: ['nuxt4'], stage: STAGE.AI_CORE, diff --git a/packages/preset-nuxt4/src/features/nuxt-ui.ts b/packages/preset-nuxt4/src/features/nuxt-ui.ts index c16cc79a..b18e96af 100644 --- a/packages/preset-nuxt4/src/features/nuxt-ui.ts +++ b/packages/preset-nuxt4/src/features/nuxt-ui.ts @@ -6,7 +6,7 @@ import { patchNuxtConfig } from '../utils/nuxt-config.js' /** Nuxt UI v4 + Tailwind v4. Registers `@nuxt/ui` module and wires `assets/css/main.css`. */ export const nuxtUiFeature: Feature = { id: 'nuxt4:nuxt-ui', - version: '1.2.2', + version: '1.2.3', label: 'Nuxt UI v4 + Tailwind v4', frameworks: ['nuxt4'], stage: STAGE.STYLING, diff --git a/packages/preset-nuxt4/src/features/prompts.ts b/packages/preset-nuxt4/src/features/prompts.ts index 6ea57614..4b10e616 100644 --- a/packages/preset-nuxt4/src/features/prompts.ts +++ b/packages/preset-nuxt4/src/features/prompts.ts @@ -6,7 +6,7 @@ import type { Feature } from '@battlestack/core' /** Admin-editable AI agent prompts. */ export const promptsFeature: Feature = { id: 'nuxt4:prompts', - version: '1.2.1', + version: '1.2.2', label: 'AI prompt management', description: 'Admin-editable agent prompts with shipped registry defaults.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/rag.ts b/packages/preset-nuxt4/src/features/rag.ts index 51a73c1f..05a66c8d 100644 --- a/packages/preset-nuxt4/src/features/rag.ts +++ b/packages/preset-nuxt4/src/features/rag.ts @@ -32,7 +32,7 @@ function defaultDimensionsFor(model: string | undefined): number { /** RAG on Mastra: chunk → embedMany → PgVector.upsert/query. pgvector ships with `nuxt4:database`. */ export const ragFeature: Feature = { id: 'nuxt4:rag', - version: '1.4.1', + version: '1.4.2', label: 'RAG (Mastra + pgvector)', description: 'Ingest, chunk, embed, and query documents with Mastra + pgvector.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/storage.ts b/packages/preset-nuxt4/src/features/storage.ts index 417264ca..3502730d 100644 --- a/packages/preset-nuxt4/src/features/storage.ts +++ b/packages/preset-nuxt4/src/features/storage.ts @@ -10,7 +10,7 @@ import { allocatePort, STAGE, type EnvVar, type Feature } from '@battlestack/cor */ export const storageFeature: Feature = { id: 'nuxt4:storage', - version: '1.3.0', + version: '1.3.1', label: 'Object storage (RustFS dev / S3 prod)', description: 'File uploads and presigned downloads; RustFS locally, S3-compatible provider in prod.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/user-admin.ts b/packages/preset-nuxt4/src/features/user-admin.ts index 4585ae12..b9ba7405 100644 --- a/packages/preset-nuxt4/src/features/user-admin.ts +++ b/packages/preset-nuxt4/src/features/user-admin.ts @@ -6,7 +6,7 @@ import { STAGE } from '@battlestack/core' /** Admin-gated user CRUD + admin pages + admin route middleware. */ export const userAdminFeature: Feature = { id: 'nuxt4:user-admin', - version: '1.0.6', + version: '1.0.7', label: 'User administration (admin-gated CRUD)', description: 'Admin-only /dashboard/users for listing, creating, editing, and deleting users.', frameworks: ['nuxt4'], diff --git a/packages/preset-nuxt4/src/features/vitest.ts b/packages/preset-nuxt4/src/features/vitest.ts index 48da1742..7b89dff1 100644 --- a/packages/preset-nuxt4/src/features/vitest.ts +++ b/packages/preset-nuxt4/src/features/vitest.ts @@ -19,7 +19,7 @@ import { ui } from '@battlestack/tui' /** Vitest config, a sample unit test and `test` scripts. Three projects: unit, nuxt, e2e. */ export const vitestFeature: Feature = { id: 'nuxt4:vitest', - version: '1.0.7', + version: '1.0.8', label: 'Vitest config + scripts', frameworks: ['nuxt4'], stage: STAGE.NAMING, // run after naming so package.json exists diff --git a/packages/preset-nuxt4/templates/audit-log/server/utils/audit.ts b/packages/preset-nuxt4/templates/audit-log/server/utils/audit.ts index 9afdecbc..2d735c3d 100644 --- a/packages/preset-nuxt4/templates/audit-log/server/utils/audit.ts +++ b/packages/preset-nuxt4/templates/audit-log/server/utils/audit.ts @@ -3,34 +3,34 @@ import { getRequestIP, getRequestHeader } from 'h3' import { db } from '#server/database/client' import { auditEvents } from '#server/database/schema/audit-log' -export type AuditAction = - | 'user.login.success' - | 'user.login.fail' - | 'user.logout' - | 'user.signup' - | 'user.deleted' - | 'user.role.changed' - | 'user.email.changed' - | 'user.password.changed' - | 'user.email.verified' - | 'user.email.verification.resent' - | 'user.password.reset.requested' - | 'user.password.reset.completed' - | 'user.totp.enabled' - | 'user.totp.disabled' - | 'user.backup-codes.generated' - | 'user.backup-codes.redeemed' - | 'user.backup-codes.redeem-failed' - | 'user.passkey.registered' - | 'user.passkey.removed' - | 'user.passkey.login' - | 'user.session.revoked' - | 'auth.oauth.signin' - | 'prompt.updated' - | 'prompt.reset' - | 'ai.model-config.updated' - | 'file.uploaded' - | 'file.deleted' +export type AuditAction + = | 'user.login.success' + | 'user.login.fail' + | 'user.logout' + | 'user.signup' + | 'user.deleted' + | 'user.role.changed' + | 'user.email.changed' + | 'user.password.changed' + | 'user.email.verified' + | 'user.email.verification.resent' + | 'user.password.reset.requested' + | 'user.password.reset.completed' + | 'user.totp.enabled' + | 'user.totp.disabled' + | 'user.backup-codes.generated' + | 'user.backup-codes.redeemed' + | 'user.backup-codes.redeem-failed' + | 'user.passkey.registered' + | 'user.passkey.removed' + | 'user.passkey.login' + | 'user.session.revoked' + | 'auth.oauth.signin' + | 'prompt.updated' + | 'prompt.reset' + | 'ai.model-config.updated' + | 'file.uploaded' + | 'file.deleted' /** Best-effort audit insert; logs to console on failure. */ export async function logAuditEvent( diff --git a/packages/preset-nuxt4/templates/auth-2fa/app/composables/use2fa.ts b/packages/preset-nuxt4/templates/auth-2fa/app/composables/use2fa.ts index 3e8195a1..5e4f5b64 100644 --- a/packages/preset-nuxt4/templates/auth-2fa/app/composables/use2fa.ts +++ b/packages/preset-nuxt4/templates/auth-2fa/app/composables/use2fa.ts @@ -1,16 +1,16 @@ /** Client wrapper around the 2FA endpoints. */ export function use2fa() { - async function status(): Promise<{ enabled: boolean; enabledAt: string | null }> { - return $fetch<{ enabled: boolean; enabledAt: string | null }>('/api/auth/2fa/status') + async function status(): Promise<{ enabled: boolean, enabledAt: string | null }> { + return $fetch<{ enabled: boolean, enabledAt: string | null }>('/api/auth/2fa/status') } - async function setup(): Promise<{ secret: string; otpauthUrl: string }> { - return $fetch<{ secret: string; otpauthUrl: string }>('/api/auth/2fa/setup', { method: 'POST' }) + async function setup(): Promise<{ secret: string, otpauthUrl: string }> { + return $fetch<{ secret: string, otpauthUrl: string }>('/api/auth/2fa/setup', { method: 'POST' }) } // Enabling 2FA also returns the freshly-generated backup codes (shown once). - async function verify(code: string): Promise<{ ok: true; backupCodes: string[] }> { - return $fetch<{ ok: true; backupCodes: string[] }>('/api/auth/2fa/verify', { method: 'POST', body: { code } }) + async function verify(code: string): Promise<{ ok: true, backupCodes: string[] }> { + return $fetch<{ ok: true, backupCodes: string[] }>('/api/auth/2fa/verify', { method: 'POST', body: { code } }) } async function disable(code: string): Promise<{ ok: true }> { diff --git a/packages/preset-nuxt4/templates/auth-2fa/server/api/auth/2fa/backup-codes/generate.post.ts b/packages/preset-nuxt4/templates/auth-2fa/server/api/auth/2fa/backup-codes/generate.post.ts index 874f75ca..7e9d42ed 100644 --- a/packages/preset-nuxt4/templates/auth-2fa/server/api/auth/2fa/backup-codes/generate.post.ts +++ b/packages/preset-nuxt4/templates/auth-2fa/server/api/auth/2fa/backup-codes/generate.post.ts @@ -35,7 +35,7 @@ export default defineEventHandler(async (event) => { setResponseHeaders(event, { 'Cache-Control': 'no-store, no-cache, must-revalidate', - Pragma: 'no-cache', + 'Pragma': 'no-cache', }) return { codes } }) diff --git a/packages/preset-nuxt4/templates/auth-2fa/server/api/auth/2fa/verify.post.ts b/packages/preset-nuxt4/templates/auth-2fa/server/api/auth/2fa/verify.post.ts index 97f10dd2..aa64e7d7 100644 --- a/packages/preset-nuxt4/templates/auth-2fa/server/api/auth/2fa/verify.post.ts +++ b/packages/preset-nuxt4/templates/auth-2fa/server/api/auth/2fa/verify.post.ts @@ -57,7 +57,7 @@ export default defineEventHandler(async (event) => { setResponseHeaders(event, { 'Cache-Control': 'no-store, no-cache, must-revalidate', - Pragma: 'no-cache', + 'Pragma': 'no-cache', }) return { ok: true, backupCodes: codes } }) diff --git a/packages/preset-nuxt4/templates/auth-2fa/server/utils/totp.ts b/packages/preset-nuxt4/templates/auth-2fa/server/utils/totp.ts index 695c9b06..9debe95f 100644 --- a/packages/preset-nuxt4/templates/auth-2fa/server/utils/totp.ts +++ b/packages/preset-nuxt4/templates/auth-2fa/server/utils/totp.ts @@ -13,8 +13,8 @@ function getEncryptionKey(): Buffer { const hex = useRuntimeConfig().totpEncryptionKey as string | undefined if (!hex || hex.length !== 64) { throw new Error( - 'NUXT_TOTP_ENCRYPTION_KEY missing or not 64 hex chars. ' + - "Generate one with `node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"` and set it in .env.", + 'NUXT_TOTP_ENCRYPTION_KEY missing or not 64 hex chars. ' + + 'Generate one with `node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"` and set it in .env.', ) } return Buffer.from(hex, 'hex') diff --git a/packages/preset-nuxt4/templates/auth-2fa/test/e2e/api/auth-2fa.test.ts b/packages/preset-nuxt4/templates/auth-2fa/test/e2e/api/auth-2fa.test.ts index b72c92cc..833a9d6c 100644 --- a/packages/preset-nuxt4/templates/auth-2fa/test/e2e/api/auth-2fa.test.ts +++ b/packages/preset-nuxt4/templates/auth-2fa/test/e2e/api/auth-2fa.test.ts @@ -27,7 +27,7 @@ describe('e2e: auth-2fa', () => { it.skipIf(!serverUp)('setup returns a secret + provisioning URL', async () => { const adminCookie = await loginAsAdmin() const user = await createTestUser(adminCookie) - const { status, data } = await apiPost<{ secret: string; otpauthUrl: string }>( + const { status, data } = await apiPost<{ secret: string, otpauthUrl: string }>( '/api/auth/2fa/setup', {}, user.cookie, diff --git a/packages/preset-nuxt4/templates/auth-recovery/app/pages/forgot-password.vue b/packages/preset-nuxt4/templates/auth-recovery/app/pages/forgot-password.vue index ea678a51..a38c776b 100644 --- a/packages/preset-nuxt4/templates/auth-recovery/app/pages/forgot-password.vue +++ b/packages/preset-nuxt4/templates/auth-recovery/app/pages/forgot-password.vue @@ -1,11 +1,22 @@