Skip to content

feat(AGE-48): Multi-step deployment wizard#39

Merged
stepandel merged 1 commit intomainfrom
feature/AGE-48-deployment-wizard
Feb 10, 2026
Merged

feat(AGE-48): Multi-step deployment wizard#39
stepandel merged 1 commit intomainfrom
feature/AGE-48-deployment-wizard

Conversation

@stepandel
Copy link
Owner

@stepandel stepandel commented Feb 10, 2026

Summary

Adds a multi-step deployment wizard at /dashboard/new that replicates the CLI init wizard as a web form.

Wizard Steps

  1. Stack & Provider — Stack name + cloud provider (AWS/Hetzner)
  2. Infrastructure — Region/location + instance type (dynamic options based on provider and region)
  3. Owner — Name, timezone, working hours
  4. Agents — Select from presets (Marcus/PM, Titus/Eng, Scout/Tester) with descriptions
  5. Review — Full config summary with estimated monthly cost, deploy button

Key Design Decisions

  • Zero duplication — All constants and types imported directly from @agent-army/cli workspace package
  • Added exports field to cli/package.json for cross-workspace imports
  • Uses React useState for form state (simple, no extra deps)
  • Inline styles (no Tailwind dependency)
  • Produces valid ArmyManifest matching cli/types.ts

Also includes

Uncommitted auth files from AGE-47 that were part of the merged PR #38 but not tracked on the branch.

Ticket

Closes AGE-48

Summary by CodeRabbit

Release Notes

  • New Features

    • Added user authentication system with login and signup functionality
    • Added protected dashboard for authenticated users
    • Added multi-step deployment wizard for creating new deployments
    • Implemented session management and automatic logout
  • Chores

    • Updated project dependencies to support authentication and database operations
    • Added database configuration for user and session storage
    • Enhanced middleware for route protection and authentication checks

- Add /dashboard/new with 5-step wizard form
  - Step 1: Stack name + cloud provider selection
  - Step 2: Region/location + instance type (dynamic per provider)
  - Step 3: Owner info (name, timezone, working hours)
  - Step 4: Agent selection from presets with descriptions
  - Step 5: Review summary with cost estimates
- Import constants and types directly from @agent-army/cli workspace package
- Add exports to cli/package.json for cross-workspace imports
- Progress indicator, back/forward navigation, field validation
- Produces valid ArmyManifest on submit

Closes AGE-48
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

Implements a comprehensive authentication system for a Next.js web application, including Prisma database schema with User, Account, Session, and VerificationToken models, NextAuth configuration with credentials provider, login and signup pages with forms, protected dashboard page, middleware-based route protection, and a multi-step deployment wizard component. Also exposes cli package exports for constants and types.

Changes

Cohort / File(s) Summary
Package Configuration & Version Control
cli/package.json, web/.gitignore, web/package.json
Exposed cli package exports for "./lib/constants" and "./types"; added gitignore rules for environment files and database artifacts; added runtime and dev dependencies for authentication (next-auth, bcryptjs, @auth/prisma-adapter) and database ORM (prisma, @prisma/client).
Database & ORM Setup
web/prisma/schema.prisma, web/src/lib/prisma.ts
Defined Prisma schema with User, Account, Session, and VerificationToken models, including relations, cascade deletes, and unique constraints; created singleton PrismaClient instance with environment-aware logging.
Authentication Core & Configuration
web/src/lib/auth.ts, web/src/types/next-auth.d.ts
Configured NextAuth with Prisma adapter and credentials provider; implemented authorize flow with email/password validation and bcrypt hashing; augmented NextAuth types to include user id in Session, User, and JWT interfaces.
Authentication API Routes
web/src/app/api/auth/[...nextauth]/route.ts, web/src/app/api/auth/signup/route.ts
Added NextAuth route handler exporting GET/POST from configured auth instance; created signup endpoint validating email/password, checking for duplicates, hashing passwords, and persisting new user records.
Authentication UI & Layout
web/src/app/login/page.tsx, web/src/app/signup/page.tsx, web/src/components/providers.tsx, web/src/app/layout.tsx
Implemented login and signup forms with form submission, error handling, and navigation; created SessionProvider wrapper for NextAuth context; wrapped root layout with Providers component.
Protected Routes & Middleware
web/src/app/dashboard/page.tsx, web/src/middleware.ts
Added dashboard page with session retrieval and auth redirect; implemented middleware enforcing authentication on /api and /dashboard routes, returning 401 for API and redirecting to /login for dashboard routes.
Deployment Wizard
web/src/app/dashboard/new/page.tsx
Created multi-step deployment wizard (385 lines) with five steps—Stack & Provider, Infrastructure, Owner, Agents, Review—including dynamic region/instance type options, cost estimation, agent selection with PRESETS integration, manifest building, and form submission to /api/deployments.

Sequence Diagram

sequenceDiagram
    participant Client as Client (Browser)
    participant Server as Server (Next.js)
    participant Auth as NextAuth Handler
    participant DB as Prisma / Database
    participant Session as Session Context

    Client->>Server: POST /api/auth/signup (email, password)
    Server->>DB: Check for existing user
    DB-->>Server: User exists or not
    alt User exists
        Server-->>Client: 409 / Error response
    else User not found
        Server->>Server: Hash password with bcryptjs
        Server->>DB: Create new User record
        DB-->>Server: User created with id, email, name
        Server-->>Client: 201 Created response
    end

    Client->>Server: POST /api/auth/callback/credentials (email, password)
    Server->>Auth: NextAuth credentials provider
    Auth->>DB: Query User by email
    DB-->>Auth: User record with hashedPassword
    Auth->>Auth: Verify password with bcryptjs
    alt Password valid
        Auth->>Auth: Create JWT token with user id
        Auth-->>Server: JWT token
        Server-->>Client: Set session cookie
        Client->>Session: Access session context
        Session-->>Client: User id, email, name available
    else Password invalid
        Auth-->>Server: Unauthorized
        Server-->>Client: 401 Unauthorized
    end

    Client->>Server: GET /dashboard (with session)
    Server->>Auth: Verify NextAuth token
    Auth-->>Server: Token valid
    Server->>Server: auth() retrieves session
    Server-->>Client: Render dashboard with greeting
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through auth-filled dreams,
With Prisma schemas and NextAuth schemes,
Login, signup, dashboards bright,
A wizard's steps, deployment flight,
Protected routes guard every way—security's play! 🔐✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(AGE-48): Multi-step deployment wizard' accurately captures the main change—a new multi-step deployment wizard component. It is clear, specific, and directly related to the primary feature added across multiple files, particularly web/src/app/dashboard/new/page.tsx.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/AGE-48-deployment-wizard

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@cli/package.json`:
- Around line 21-25: The package.json exports currently point to source .ts
files (exports like "./lib/constants": "./lib/constants.ts" and "./types":
"./types.ts"), which will break consumers when only dist is published; update
the "exports" mapping to reference the compiled outputs under dist (e.g. map
"./lib/constants" to the compiled JS at dist/lib/constants.js and its types at
dist/lib/constants.d.ts, and map "./types" to dist/types.js with
dist/types.d.ts) so module resolution and type declarations work for consumers
after tsc-outDir compilation.

In `@web/package.json`:
- Around line 13-18: Your package.json currently pins next-auth to
"5.0.0-beta.30" (next-auth) which is unstable; either replace that dependency
with the stable v4 ("4.24.13") or add an explicit rationale in project docs and
add CI tests for auth flows to cover App Router behaviors; also add a
max-password-length validation wherever passwords are handled (e.g.,
registerUser / validatePassword or auth controller) because bcryptjs truncates
at 72 bytes, and confirm `@prisma/client` ("5.22.0") is intentionally chosen or
update if needed. Ensure changes include updating package.json dependency,
adding a short README note about the v5-beta choice if kept, and adding
unit/integration tests for authentication paths and a password-length check in
the password-handling functions.

In `@web/src/app/api/auth/signup/route.ts`:
- Around line 7-57: Validate that email and password are strings (e.g., typeof
email === "string" && typeof password === "string"), trim and normalize the
email to lowercase before use, and only proceed to bcrypt.hash after those
checks; keep the prisma.user.findUnique existence check but wrap
prisma.user.create in a try/catch that specifically detects a
Prisma.PrismaClientKnownRequestError with code "P2002" (unique constraint) and
return NextResponse.json({ error: "User with this email already exists" }, {
status: 409 }) for that case, while rethrowing or returning a 500 for other
errors—update the final catch block to discriminate P2002 vs generic errors and
return 409 when encountered.

In `@web/src/app/dashboard/new/page.tsx`:
- Around line 68-72: The agent selection UI uses a clickable <div> with a
readOnly checkbox which blocks keyboard users; update the selection markup so it
is keyboard-accessible—either wrap the checkbox and content in a <label> and
wire the checkbox's onChange to the existing selection handler (remove
readOnly), or make the clickable container interactive by adding role="button",
tabIndex={0}, and an onKeyDown handler that toggles selection on Enter/Space and
calls the same selection function; ensure the visual style referenced by
agentCard remains unchanged and that the checkbox input and the container call
the same selection method so both mouse and keyboard trigger selection.
- Around line 98-115: In validate(), trim state.stackName once and use that
trimmed value for both checks to avoid false pattern errors and duplicates; e.g.
const name = state.stackName.trim(); then if (step === 0) { if (!name)
errs.push("Stack name is required"); else if (!/^[a-z0-9-]+$/.test(name))
errs.push("Stack name: lowercase letters, numbers, hyphens only"); } — update
references to state.stackName accordingly so validate() uses the trimmed name
for validation.

In `@web/src/app/dashboard/page.tsx`:
- Around line 17-28: Replace the GET anchor logout link with a client-side
logout button component that calls signOut from next-auth/react: create a "use
client" component named LogoutButton (e.g., export function LogoutButton) which
imports signOut and invokes signOut({ redirectTo: "/login" }) in an onClick
handler, apply the existing styles to a <button> (remove href/anchor usage), and
then import and render LogoutButton in page.tsx instead of the anchor to ensure
proper POST logout behavior in NextAuth v5.
🧹 Nitpick comments (3)
web/src/app/login/page.tsx (1)

34-35: Unused variable in catch block.

The error variable is caught but never used. Consider logging it for debugging or using an underscore prefix to indicate it's intentionally ignored.

🔧 Suggested fix
-    } catch (error) {
+    } catch (_error) {
       setError("An error occurred. Please try again.");
web/prisma/schema.prisma (2)

12-21: Consider making hashedPassword optional for future OAuth support.

Currently hashedPassword is required (String), but if OAuth providers (Google, GitHub, etc.) are added later, those users won't have passwords. Making it optional (String?) would future-proof the schema.

🔧 Suggested change
 model User {
   id             String    `@id` `@default`(cuid())
   email          String    `@unique`
   name           String?
-  hashedPassword String
+  hashedPassword String?
   createdAt      DateTime  `@default`(now())
   updatedAt      DateTime  `@updatedAt`
   sessions       Session[]
   accounts       Account[]
 }

Note: The check in auth.ts (!user.hashedPassword) already handles this case defensively.


7-10: SQLite is suitable for development; consider production database strategy.

SQLite works well for local development and small deployments. Ensure there's a plan for migrating to a production-ready database (PostgreSQL, MySQL) when scaling, as SQLite has concurrency limitations.

Comment on lines +21 to +25
"license": "MIT",
"exports": {
"./lib/constants": "./lib/constants.ts",
"./types": "./types.ts"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n cli/package.json

Repository: stepandel/agent-army

Length of output: 841


🏁 Script executed:

find dist -type f \( -name "*.js" -o -name "*.d.ts" \) 2>/dev/null | head -20 || echo "dist directory not found or empty"

Repository: stepandel/agent-army

Length of output: 46


🏁 Script executed:

# Check if there's a build script and any TypeScript config
cat -n cli/tsconfig.json 2>/dev/null | head -30 || echo "No tsconfig.json found"

Repository: stepandel/agent-army

Length of output: 602


🏁 Script executed:

# Check for the source files referenced in exports
ls -la cli/lib/constants.ts 2>/dev/null || echo "cli/lib/constants.ts not found"
ls -la cli/types.ts 2>/dev/null || echo "cli/types.ts not found"

Repository: stepandel/agent-army

Length of output: 195


🏁 Script executed:

# Check dist directory contents
ls -la cli/dist/ 2>/dev/null || echo "cli/dist directory not found or empty"

Repository: stepandel/agent-army

Length of output: 101


🏁 Script executed:

# Check if dist directory even exists
test -d cli/dist && echo "dist directory exists" || echo "dist directory does not exist"

Repository: stepandel/agent-army

Length of output: 93


Exporting TS sources will break consumers when only dist is published.

The "exports" field maps to ./lib/constants.ts and ./types.ts, but "files" includes only dist. When the package is built with tsc (configured with "outDir": "./dist"), these files will be compiled to ./dist/lib/constants.js, ./dist/lib/constants.d.ts, etc., which won't match the export paths. Consumers will encounter module resolution failures. Update exports to reference the compiled outputs in dist with corresponding type declarations.

🔧 Suggested fix (map exports to compiled outputs)
   "exports": {
-    "./lib/constants": "./lib/constants.ts",
-    "./types": "./types.ts"
+    "./lib/constants": {
+      "types": "./dist/lib/constants.d.ts",
+      "default": "./dist/lib/constants.js"
+    },
+    "./types": {
+      "types": "./dist/types.d.ts",
+      "default": "./dist/types.js"
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"license": "MIT",
"exports": {
"./lib/constants": "./lib/constants.ts",
"./types": "./types.ts"
}
"license": "MIT",
"exports": {
"./lib/constants": {
"types": "./dist/lib/constants.d.ts",
"default": "./dist/lib/constants.js"
},
"./types": {
"types": "./dist/types.d.ts",
"default": "./dist/types.js"
}
}
🤖 Prompt for AI Agents
In `@cli/package.json` around lines 21 - 25, The package.json exports currently
point to source .ts files (exports like "./lib/constants": "./lib/constants.ts"
and "./types": "./types.ts"), which will break consumers when only dist is
published; update the "exports" mapping to reference the compiled outputs under
dist (e.g. map "./lib/constants" to the compiled JS at dist/lib/constants.js and
its types at dist/lib/constants.d.ts, and map "./types" to dist/types.js with
dist/types.d.ts) so module resolution and type declarations work for consumers
after tsc-outDir compilation.

Comment on lines +13 to 18
"@auth/prisma-adapter": "^2.11.1",
"@prisma/client": "5.22.0",
"bcryptjs": "^3.0.3",
"next": "^14.2.0",
"next-auth": "5.0.0-beta.30",
"react": "^18.3.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

next-auth latest stable version 2025 production ready

💡 Result:

As of Feb 10, 2026, the latest stable (production) release of next-auth (NextAuth.js/Auth.js) on npm is:

  • next-auth@4.24.13 (stable) — released Oct 29, 2025. [1][2]
  • The v5 line is still beta; the latest beta shown is 5.0.0-beta.30 (released Oct 27, 2025). [1][3]

Production-ready recommendation (2025/2026):

  • If you want “stable” specifically, use 4.24.13. [1]
  • Note: the maintainers state Auth.js (formerly NextAuth.js) is now maintained by the Better Auth team and recommend new projects start with Better Auth unless you have specific gaps. [2]
  • Security note: GitHub’s advisory shows fixes landed in 4.24.12 and 5.0.0-beta.30 for an email sign-in misdelivery issue, so you should be on at least those versions if affected. [3]

Sources: [1] [2] [3]


🌐 Web query:

next-auth 5.0.0-beta.30 vs next-auth 5.0.0 stable release date

💡 Result:

  • next-auth@5.0.0-beta.30 release date: October 27, 2025 (published as a beta) [1][2]
  • next-auth@5.0.0 (stable) release date: Not released / no stable 5.0.0 published as of February 10, 2026—the latest stable line shown is still 4.24.13 (October 29, 2025) [1]

Sources:
[1] next-auth release history (NPM release tracker) https://releasealert.dev/npm/_/next-auth
[2] GitHub Advisory noting patched version 5.0.0-beta.30 published Oct 27, 2025 GHSA-5jpx-9hw9-2fx4


🌐 Web query:

bcryptjs 3.0.3 security advisories vulnerabilities

💡 Result:

bcryptjs@3.0.3 — known security advisories / vulnerabilities

  • Direct vulnerabilities in bcryptjs@3.0.3: none currently listed in Snyk’s vulnerability database (Snyk shows “No direct vulnerabilities” and lists 3.0.3 as the latest non‑vulnerable version). [1]

  • Important security footgun (design property): 72-byte password truncation. bcryptjs (like bcrypt generally) ignores bytes beyond 72, so applications that don’t enforce a max password length can create surprising authentication behavior for very long passwords. This has shown up as a vulnerability in downstream applications (example: CVE-2025-25298 in Strapi, fixed by adding max-length validation rather than changing bcryptjs). [2][3]

Practical guidance

  • If you use bcryptjs@3.0.3, enforce a maximum password length of 72 bytes (or fewer) (and handle Unicode/UTF-8 bytes explicitly) to avoid truncation surprises described above. [2]
  • Also run your usual dependency scanning (npm audit, Dependabot, Snyk) for indirect vulnerabilities (even though bcryptjs itself is “zero dependencies”). [1]

Sources: Snyk package advisory status for bcryptjs@3.0.3 [1]; NVD record for CVE-2025-25298 (downstream misuse of bcryptjs length behavior) [2]; Tenable summary referencing the associated advisory [3].

[1] https://security.snyk.io/package/npm/bcryptjs/3.0.3
[2] https://nvd.nist.gov/vuln/detail/CVE-2025-25298
[3] https://www.tenable.com/cve/CVE-2025-25298


🌐 Web query:

Prisma 5.22.0 security advisories vulnerabilities

💡 Result:

As of Feb 10, 2026, there are no known direct security advisories / CVEs specifically affecting prisma@5.22.0 or @prisma/client@5.22.0 in Snyk’s vulnerability database. [1][2]

The Prisma GitHub repo’s published security advisory is GHSA-pxcc-hj8w-fmm7 (CVE-2021-21414), a command-injection issue in @prisma/sdk affecting versions < 2.20.0 (patched in 2.20.0), which is far older than Prisma 5.22.0. [3]

If you’re auditing a real project pinned to Prisma 5.22.0, you should still check transitive dependency advisories via npm audit/pnpm audit (Snyk’s “no direct vulnerabilities” does not rule out indirect ones). [1][2]

Sources: [1] prisma@5.22.0 (Snyk) • [2] @prisma/client@5.22.0 (Snyk) • [3] GHSA-pxcc-hj8w-fmm7 / CVE-2021-21414 (Prisma GitHub Security Advisory)


🌐 Web query:

next-auth beta Next.js 14 production use recommended

💡 Result:

Using next-auth@beta (Auth.js / NextAuth v5) with Next.js 14 can work in production, but it’s not the “recommended” low-risk choice because it’s still a beta line.

  • What’s officially clear: v5 is installed via npm install next-auth@beta, is “App Router-first”, and requires Next.js ≥ 14.0. [1]
  • Stability reality check: the stable line is still v4 (4.24.x), while v5 continues to ship as 5.0.0-beta.* (no stable 5.0.0 in the public release stream shown). [2]
  • Practical guidance from community usage: people do run v5 beta in production successfully, but commonly advise v4 for maximum stability and v5 only if you need v5/App Router-first features, with caution. [3]
  • Why caution: there are ongoing v5-specific issues/edge-cases reported (example: middleware/react/Next version interactions). [4]

Recommendation (production):

  • If you want the safest/most conventional production setup on Next.js 14: use next-auth@4.24.x. [2]
  • If you specifically need v5 features (notably the new auth()-centric APIs and App Router-first approach): you can use next-auth@beta, but treat it as higher risk (pin an exact beta version, add strong test coverage for auth flows, and watch upstream issues). [1][3]

Sources:
[1] Auth.js “Migrating to v5” (shows next-auth@beta, App Router-first, Next.js ≥14) (authjs.cn)
[2] next-auth release stream showing v4 stable vs v5 beta versions (releasealert.dev)
[3] Next.js Discord forum thread discussing v5 beta production suitability (nextjs-forum.com)
[4] next-auth GitHub issue example (v5 beta + Next version/middleware issues) (github.com)

Citations:


Consider upgrading to stable next-auth@4.24.13 or confirm v5-beta is intentional.

Line 17 pins next-auth@5.0.0-beta.30, which has no stable v5 release as of February 2026; the stable line remains v4 (4.24.13). If you need v5-specific features (App Router-first APIs), the beta is functional but carries higher risk and fewer production examples. Recommend either upgrading to v4 for maximum stability, or explicitly documenting the v5 choice with test coverage for auth flows.

No security advisories found for bcryptjs@3.0.3 or Prisma@5.22.0 directly; note that bcryptjs truncates passwords at 72 bytes (inherent to bcrypt), so enforce a max-length validation in your password handling.

Also applies to: 22-26

🤖 Prompt for AI Agents
In `@web/package.json` around lines 13 - 18, Your package.json currently pins
next-auth to "5.0.0-beta.30" (next-auth) which is unstable; either replace that
dependency with the stable v4 ("4.24.13") or add an explicit rationale in
project docs and add CI tests for auth flows to cover App Router behaviors; also
add a max-password-length validation wherever passwords are handled (e.g.,
registerUser / validatePassword or auth controller) because bcryptjs truncates
at 72 bytes, and confirm `@prisma/client` ("5.22.0") is intentionally chosen or
update if needed. Ensure changes include updating package.json dependency,
adding a short README note about the v5-beta choice if kept, and adding
unit/integration tests for authentication paths and a password-length check in
the password-handling functions.

Comment on lines +7 to +57
const body = await req.json();
const { email, password, name } = body;

if (!email || !password) {
return NextResponse.json(
{ error: "Email and password are required" },
{ status: 400 }
);
}

// Check if user already exists
const existingUser = await prisma.user.findUnique({
where: { email },
});

if (existingUser) {
return NextResponse.json(
{ error: "User with this email already exists" },
{ status: 400 }
);
}

// Hash the password
const hashedPassword = await bcrypt.hash(password, 10);

// Create the user
const user = await prisma.user.create({
data: {
email,
name,
hashedPassword,
},
});

// Return user without password
return NextResponse.json(
{
user: {
id: user.id,
email: user.email,
name: user.name,
},
},
{ status: 201 }
);
} catch (error) {
console.error("Signup error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "route.ts" -path "*/auth/signup/*" | head -20

Repository: stepandel/agent-army

Length of output: 102


🏁 Script executed:

git ls-files | grep -E "signup.*route\.ts|route\.ts.*signup"

Repository: stepandel/agent-army

Length of output: 100


🏁 Script executed:

find . -type f -name "route.ts" | xargs grep -l "signup\|POST.*email.*password" 2>/dev/null | head -10

Repository: stepandel/agent-army

Length of output: 46


🏁 Script executed:

cat -n web/src/app/api/auth/signup/route.ts

Repository: stepandel/agent-army

Length of output: 1801


Harden input validation and handle concurrent signup race (P2002).

Line 8 lacks type safety—non-string values like email: 123 or password: {} pass the truthy check but will crash downstream. Lines 18–27 expose a TOCTOU race: if two requests attempt signup with the same email, both may pass the existence check before the first completes, causing Prisma to throw a P2002 unique-constraint error. The catch block (lines 52–57) doesn't distinguish P2002 from other errors, returning 500 instead of 409. Add explicit type checks, email normalization, and catch P2002 with a 409 response.

🛠️ Suggested fix
-import { prisma } from "@/lib/prisma";
+import { prisma } from "@/lib/prisma";
+import { Prisma } from "@prisma/client";
 
 export async function POST(req: NextRequest) {
   try {
     const body = await req.json();
-    const { email, password, name } = body;
+    const { email, password, name } = body ?? {};
+
+    if (typeof email !== "string" || typeof password !== "string") {
+      return NextResponse.json(
+        { error: "Email and password are required" },
+        { status: 400 }
+      );
+    }
+
+    const normalizedEmail = email.trim().toLowerCase();
+    const normalizedName = typeof name === "string" ? name.trim() : undefined;
+    if (!normalizedEmail || password.length < 6) {
+      return NextResponse.json(
+        { error: "Email and password are required" },
+        { status: 400 }
+      );
+    }
 
     // Check if user already exists
     const existingUser = await prisma.user.findUnique({
-      where: { email },
+      where: { email: normalizedEmail },
     });
 
     if (existingUser) {
       return NextResponse.json(
-        { error: "User with this email already exists" },
-        { status: 400 }
+        { error: "Unable to create account" },
+        { status: 409 }
       );
     }
 
     // Hash the password
     const hashedPassword = await bcrypt.hash(password, 10);
 
     // Create the user
     const user = await prisma.user.create({
       data: {
-        email,
-        name,
+        email: normalizedEmail,
+        name: normalizedName,
         hashedPassword,
       },
     });
@@
-  } catch (error) {
+  } catch (error) {
+    if (
+      error instanceof Prisma.PrismaClientKnownRequestError &&
+      error.code === "P2002"
+    ) {
+      return NextResponse.json(
+        { error: "Unable to create account" },
+        { status: 409 }
+      );
+    }
     console.error("Signup error:", error);
     return NextResponse.json(
       { error: "Internal server error" },
       { status: 500 }
     );
   }
 }
🤖 Prompt for AI Agents
In `@web/src/app/api/auth/signup/route.ts` around lines 7 - 57, Validate that
email and password are strings (e.g., typeof email === "string" && typeof
password === "string"), trim and normalize the email to lowercase before use,
and only proceed to bcrypt.hash after those checks; keep the
prisma.user.findUnique existence check but wrap prisma.user.create in a
try/catch that specifically detects a Prisma.PrismaClientKnownRequestError with
code "P2002" (unique constraint) and return NextResponse.json({ error: "User
with this email already exists" }, { status: 409 }) for that case, while
rethrowing or returning a 500 for other errors—update the final catch block to
discriminate P2002 vs generic errors and return 409 when encountered.

Comment on lines +68 to +72
agentCard: (selected: boolean) => ({
padding: 16, borderRadius: 8, marginBottom: 8, cursor: "pointer",
border: `2px solid ${selected ? "#3b82f6" : "#e5e7eb"}`,
background: selected ? "#eff6ff" : "#fff",
}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make agent selection keyboard-accessible.
The clickable <div> plus readOnly checkbox prevents keyboard toggling. Use a <label> with an onChange handler (or add role, tabIndex, and key handlers) so keyboard users can select agents.

♿ Suggested fix
-  agentCard: (selected: boolean) => ({
+  agentCard: (selected: boolean) => ({
     padding: 16, borderRadius: 8, marginBottom: 8, cursor: "pointer",
     border: `2px solid ${selected ? "#3b82f6" : "#e5e7eb"}`,
     background: selected ? "#eff6ff" : "#fff",
+    display: "block",
   }),
-                <div
-                  key={key}
-                  style={s.agentCard(selected)}
-                  onClick={() => toggleAgent(key)}
-                >
+                <label
+                  key={key}
+                  style={s.agentCard(selected)}
+                >
                   <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
                     <input
                       type="checkbox"
                       checked={selected}
-                      readOnly
+                      onChange={() => toggleAgent(key)}
                       style={{ width: 18, height: 18 }}
                     />
                     <div>
                       <div style={{ fontWeight: 600, fontSize: 15 }}>
                         {preset.displayName}{" "}
                         <span style={{ fontWeight: 400, color: "#6b7280" }}>
                           ({preset.role})
                         </span>
                       </div>
                       <div style={{ fontSize: 13, color: "#6b7280", marginTop: 2 }}>
                         {preset.description}
                       </div>
                     </div>
                   </div>
-                </div>
+                </label>

Also applies to: 305-329

🤖 Prompt for AI Agents
In `@web/src/app/dashboard/new/page.tsx` around lines 68 - 72, The agent selection
UI uses a clickable <div> with a readOnly checkbox which blocks keyboard users;
update the selection markup so it is keyboard-accessible—either wrap the
checkbox and content in a <label> and wire the checkbox's onChange to the
existing selection handler (remove readOnly), or make the clickable container
interactive by adding role="button", tabIndex={0}, and an onKeyDown handler that
toggles selection on Enter/Space and calls the same selection function; ensure
the visual style referenced by agentCard remains unchanged and that the checkbox
input and the container call the same selection method so both mouse and
keyboard trigger selection.

Comment on lines +98 to +115
function validate(): string[] {
const errs: string[] = [];
if (step === 0) {
if (!state.stackName.trim()) errs.push("Stack name is required");
if (!/^[a-z0-9-]+$/.test(state.stackName)) errs.push("Stack name: lowercase letters, numbers, hyphens only");
}
if (step === 1) {
if (!state.region) errs.push("Region is required");
if (!state.instanceType) errs.push("Instance type is required");
}
if (step === 2) {
if (!state.ownerName.trim()) errs.push("Owner name is required");
}
if (step === 3) {
if (state.selectedAgents.length === 0) errs.push("Select at least one agent");
}
return errs;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate the trimmed stack name to avoid false errors.
Line 101–103 validates the raw string; leading/trailing spaces can produce a pattern error even when the trimmed value is valid, and you can also end up with duplicate errors.

✅ Suggested fix
 function validate(): string[] {
   const errs: string[] = [];
   if (step === 0) {
-    if (!state.stackName.trim()) errs.push("Stack name is required");
-    if (!/^[a-z0-9-]+$/.test(state.stackName)) errs.push("Stack name: lowercase letters, numbers, hyphens only");
+    const stack = state.stackName.trim();
+    if (!stack) {
+      errs.push("Stack name is required");
+    } else if (!/^[a-z0-9-]+$/.test(stack)) {
+      errs.push("Stack name: lowercase letters, numbers, hyphens only");
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function validate(): string[] {
const errs: string[] = [];
if (step === 0) {
if (!state.stackName.trim()) errs.push("Stack name is required");
if (!/^[a-z0-9-]+$/.test(state.stackName)) errs.push("Stack name: lowercase letters, numbers, hyphens only");
}
if (step === 1) {
if (!state.region) errs.push("Region is required");
if (!state.instanceType) errs.push("Instance type is required");
}
if (step === 2) {
if (!state.ownerName.trim()) errs.push("Owner name is required");
}
if (step === 3) {
if (state.selectedAgents.length === 0) errs.push("Select at least one agent");
}
return errs;
}
function validate(): string[] {
const errs: string[] = [];
if (step === 0) {
const stack = state.stackName.trim();
if (!stack) {
errs.push("Stack name is required");
} else if (!/^[a-z0-9-]+$/.test(stack)) {
errs.push("Stack name: lowercase letters, numbers, hyphens only");
}
}
if (step === 1) {
if (!state.region) errs.push("Region is required");
if (!state.instanceType) errs.push("Instance type is required");
}
if (step === 2) {
if (!state.ownerName.trim()) errs.push("Owner name is required");
}
if (step === 3) {
if (state.selectedAgents.length === 0) errs.push("Select at least one agent");
}
return errs;
}
🤖 Prompt for AI Agents
In `@web/src/app/dashboard/new/page.tsx` around lines 98 - 115, In validate(),
trim state.stackName once and use that trimmed value for both checks to avoid
false pattern errors and duplicates; e.g. const name = state.stackName.trim();
then if (step === 0) { if (!name) errs.push("Stack name is required"); else if
(!/^[a-z0-9-]+$/.test(name)) errs.push("Stack name: lowercase letters, numbers,
hyphens only"); } — update references to state.stackName accordingly so
validate() uses the trimmed name for validation.

Comment on lines +17 to +28
<a
href="/api/auth/signout"
style={{
padding: "10px 20px",
backgroundColor: "#f44336",
color: "white",
textDecoration: "none",
borderRadius: "4px",
display: "inline-block",
}}
>
Logout
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "page.tsx" | grep dashboard

Repository: stepandel/agent-army

Length of output: 133


🏁 Script executed:

find . -type f -name "package.json" | head -5

Repository: stepandel/agent-army

Length of output: 116


🏁 Script executed:

find . -type f -name "*.config.*" | grep -E "(auth|next)" | head -10

Repository: stepandel/agent-army

Length of output: 84


🏁 Script executed:

cat ./web/src/app/dashboard/page.tsx

Repository: stepandel/agent-army

Length of output: 893


🏁 Script executed:

cat ./web/package.json | head -50

Repository: stepandel/agent-army

Length of output: 732


🏁 Script executed:

find . -type f -name "auth.ts" -o -name "auth.js" -o -name "[auth]" | grep -v node_modules

Repository: stepandel/agent-army

Length of output: 85


🏁 Script executed:

cat ./web/src/lib/auth.ts

Repository: stepandel/agent-army

Length of output: 1734


🏁 Script executed:

cat -n ./web/src/app/dashboard/page.tsx | sed -n '17,28p'

Repository: stepandel/agent-army

Length of output: 460


🏁 Script executed:

cat ./web/src/app/dashboard/page.tsx | wc -l

Repository: stepandel/agent-army

Length of output: 66


🏁 Script executed:

find . -type f -name "*.tsx" -o -name "*.ts" | xargs grep -l "signOut" | grep -v node_modules

Repository: stepandel/agent-army

Length of output: 85


🏁 Script executed:

find . -type f -path "*/app/api/auth/*" | head -20

Repository: stepandel/agent-army

Length of output: 148


🏁 Script executed:

cat ./web/src/app/api/auth/[...nextauth]/route.ts

Repository: stepandel/agent-army

Length of output: 142


🏁 Script executed:

web_search
NextAuth v5 beta /api/auth/signout GET vs POST behavior

Repository: stepandel/agent-army

Length of output: 161


🌐 Web query:

NextAuth v5 /api/auth/signout endpoint GET vs POST CSRF protection

💡 Result:

In NextAuth/Auth.js v5, /api/auth/signout has two different behaviors:

  • GET /api/auth/signout: renders the sign-out page (a confirmation UI). It is not the state-changing logout action. [1]
  • POST /api/auth/signout: actually signs the user out (clears the session cookie; deletes DB session if used). Because it changes auth state, it is CSRF-protected. [1]

Why POST needs CSRF (and why GET doesn’t log you out)

Auth.js uses the double-submit cookie approach: you must first obtain a CSRF token (which also sets a CSRF cookie), then send that token back in the POST. The CSRF token must be submitted as csrfToken with all POST submissions to auth endpoints. [2]

What to do in practice

  • Preferred: call the client helper signOut() (it handles CSRF for you).
  • If you must call the endpoint yourself:
    1. GET /api/auth/csrf (store cookies)
    2. POST /api/auth/signout including csrfToken (and include cookies via credentials: "include"). If you don’t, you’ll typically get redirected to a CSRF flow / signout page and the session may remain. [3]

Sources: Auth.js endpoint semantics for signout GET vs POST [1], CSRF token/cookie requirement (“double submit cookie method”) [2], example of manual POST signout requiring CSRF token and avoiding unwanted redirects [3].

[1] (authjs.cn)
[2] (github.com)
[3] (stackoverflow.com)

Citations:


🌐 Web query:

NextAuth 5.0.0-beta.30 signOut function next-auth/react logout

💡 Result:

In NextAuth/Auth.js v5 (e.g. next-auth@5.0.0-beta.30), “logout” on the client is done with signOut from next-auth/react.

"use client"

import { signOut } from "next-auth/react"

export function LogoutButton() {
  return (
    <button onClick={() => signOut({ redirectTo: "/login" })}>
      Logout
    </button>
  )
}

Notes:

  • Make sure you call it: signOut() (not signOut). [1]
  • In v5, callbackUrl is deprecated; use redirectTo instead. [2]
  • To sign out without a navigation:
    await signOut({ redirect: false })
    (then do your own router.push(...) if you want). [2]

Sources: [1] [2]


Use signOut() client function instead of GET link to properly log out.

Line 18 uses a GET link to /api/auth/signout. In NextAuth v5, GET only renders a confirmation page and doesn't actually sign out; only POST (with CSRF protection) performs the logout. The correct approach is a client-side button that calls signOut() from next-auth/react.

Create a logout button component:

Suggested approach
// web/src/components/logout-button.tsx
"use client";
import { signOut } from "next-auth/react";

export function LogoutButton() {
  return (
    <button
      onClick={() => signOut({ redirectTo: "/login" })}
      style={{
        padding: "10px 20px",
        backgroundColor: "#f44336",
        color: "white",
        border: "none",
        borderRadius: "4px",
      }}
    >
      Logout
    </button>
  );
}
-        <a
-          href="/api/auth/signout"
-          style={{
-            padding: "10px 20px",
-            backgroundColor: "#f44336",
-            color: "white",
-            textDecoration: "none",
-            borderRadius: "4px",
-            display: "inline-block",
-          }}
-        >
-          Logout
-        </a>
+        <LogoutButton />
🤖 Prompt for AI Agents
In `@web/src/app/dashboard/page.tsx` around lines 17 - 28, Replace the GET anchor
logout link with a client-side logout button component that calls signOut from
next-auth/react: create a "use client" component named LogoutButton (e.g.,
export function LogoutButton) which imports signOut and invokes signOut({
redirectTo: "/login" }) in an onClick handler, apply the existing styles to a
<button> (remove href/anchor usage), and then import and render LogoutButton in
page.tsx instead of the anchor to ensure proper POST logout behavior in NextAuth
v5.

@stepandel stepandel merged commit 3a3ed56 into main Feb 10, 2026
1 check passed
@stepandel stepandel deleted the feature/AGE-48-deployment-wizard branch February 10, 2026 06:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant