Flatten the repo: move everything out of app/ to the root
Now that the CLI and the Next.js app are one package, nesting it inside app/ served no purpose - the repo root itself becomes the published npm package. Merges app/.gitignore and app/README.md into the root versions, drops the now-duplicate app/LICENSE, and updates path references (README, docs/ARCHITECTURE.md, docs/CONFIG_REFERENCE.md, package.json's repository.directory) that assumed the app/ nesting. Also fixes a real bug this surfaced: the in-app docs viewer resolved docs/ relative to process.cwd(), which only worked by accident when the CLI happened to be invoked from app/'s parent directory. A first attempt at fixing it with import.meta.dirname broke instead, for the same cross-module-graph reason config-path resolution already documented - Next compiles Route Handlers through a separate module graph that doesn't preserve source-relative import.meta paths. Fixed by exposing the app root via TRIGGERSHELL_APP_ROOT (set once in server.ts, where import.meta *does* resolve correctly), the same pattern already used for TRIGGERSHELL_CONFIG_PATH. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { getDb } from "@/lib/db/client";
|
||||
import { users } from "@/lib/db/schema";
|
||||
import { verifyPassword } from "@/lib/auth/password";
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
import {
|
||||
isRateLimited,
|
||||
recordFailedAttempt,
|
||||
clearAttempts,
|
||||
} from "@/lib/auth/rate-limit";
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const rateLimitKey = request.headers.get("x-forwarded-for") ?? "local";
|
||||
if (isRateLimited(rateLimitKey)) {
|
||||
return Response.json(
|
||||
{ error: "Too many attempts, try again later." },
|
||||
{ status: 429 },
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = loginSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return Response.json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const user = db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, parsed.data.username))
|
||||
.get();
|
||||
|
||||
if (
|
||||
!user ||
|
||||
!(await verifyPassword(user.passwordHash, parsed.data.password))
|
||||
) {
|
||||
recordFailedAttempt(rateLimitKey);
|
||||
return Response.json({ error: "Invalid credentials" }, { status: 401 });
|
||||
}
|
||||
|
||||
clearAttempts(rateLimitKey);
|
||||
db.update(users)
|
||||
.set({ lastLoginAt: new Date() })
|
||||
.where(eq(users.id, user.id))
|
||||
.run();
|
||||
|
||||
const session = await getSession();
|
||||
session.userId = user.id;
|
||||
session.username = user.username;
|
||||
await session.save();
|
||||
|
||||
return Response.json({ user: { username: user.username } });
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { getSession } from "@/lib/auth/session";
|
||||
|
||||
export async function POST() {
|
||||
const session = await getSession();
|
||||
session.destroy();
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { requireAuth } from "@/lib/auth/guard";
|
||||
import { getConfig } from "@/lib/config/load";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { config } = getConfig();
|
||||
const auth = await requireAuth(request);
|
||||
|
||||
return Response.json({
|
||||
authRequired: config.auth.enabled,
|
||||
authenticated: auth.authenticated,
|
||||
user:
|
||||
auth.authenticated && auth.identity ? { username: auth.identity } : null,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user