fix: auth login flow, Next.js 16 upgrade, ESM/CJS compatibility

- Upgrade Next.js 15 → 16.1 and rename middleware.ts → proxy.ts
- Fix ESM/CJS compatibility: remove "type": "module" from config/shared packages, switch to commonjs
- Fix Better Auth integration: text IDs for sessions/accounts/verifications, wildcard route @All("**"), proper password hash for admin seed
- Fix auth-client to use window.location.origin instead of hardcoded localhost:4000
- Fix api-client, forgot-password, reset-password to use relative /api/ URLs (via Next.js proxy)
- Fix TimeoutInterceptor DI by using useValue instead of useClass
- Add Playwright E2E login tests for localhost and v2.sase.tr
- Add missing UI components: vin-input, vehicle-card, category-tree

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-02-12 02:54:15 +00:00
parent 56a3c8bfaa
commit ffa9781eb9
24 changed files with 550 additions and 75 deletions

View File

@@ -18,7 +18,7 @@ export default function ForgotPasswordPage() {
setLoading(true);
try {
await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000/api"}/auth/forget-password`, {
await fetch("/api/auth/forget-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, redirectTo: "/reset-password" }),

View File

@@ -21,7 +21,7 @@ function ResetPasswordForm() {
setLoading(true);
try {
await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000/api"}/auth/reset-password`, {
await fetch("/api/auth/reset-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ newPassword: password, token }),

View File

@@ -0,0 +1,108 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { ChevronRight, ChevronDown, FolderOpen, Folder } from "lucide-react";
interface Category {
id: string;
name: string;
children?: Category[];
partCount?: number;
}
interface CategoryTreeProps {
categories: Category[];
vehicleId: string;
basePath?: string;
}
export function CategoryTree({ categories, vehicleId, basePath }: CategoryTreeProps) {
if (!categories || categories.length === 0) {
return (
<p className="py-4 text-center text-sm text-muted-foreground">
Kategori bulunamadi.
</p>
);
}
return (
<div className="space-y-1">
{categories.map((category) => (
<CategoryNode
key={category.id}
category={category}
vehicleId={vehicleId}
basePath={basePath}
level={0}
/>
))}
</div>
);
}
interface CategoryNodeProps {
category: Category;
vehicleId: string;
basePath?: string;
level: number;
}
function CategoryNode({ category, vehicleId, basePath, level }: CategoryNodeProps) {
const [expanded, setExpanded] = useState(false);
const hasChildren = category.children && category.children.length > 0;
const href = basePath
? `${basePath}/${category.id}`
: `/vehicles/${vehicleId}/categories/${category.id}`;
return (
<div>
<div
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent"
style={{ paddingLeft: `${level * 16 + 8}px` }}
>
{hasChildren ? (
<button
type="button"
onClick={() => setExpanded(!expanded)}
className="flex h-5 w-5 items-center justify-center rounded hover:bg-muted"
>
{expanded ? (
<ChevronDown className="h-3.5 w-3.5" />
) : (
<ChevronRight className="h-3.5 w-3.5" />
)}
</button>
) : (
<span className="h-5 w-5" />
)}
{expanded ? (
<FolderOpen className="h-4 w-4 text-muted-foreground" />
) : (
<Folder className="h-4 w-4 text-muted-foreground" />
)}
<Link href={href} className="flex-1 truncate hover:underline">
{category.name}
</Link>
{category.partCount != null && category.partCount > 0 && (
<span className="text-xs text-muted-foreground">
{category.partCount}
</span>
)}
</div>
{hasChildren && expanded && (
<div>
{category.children!.map((child) => (
<CategoryNode
key={child.id}
category={child}
vehicleId={vehicleId}
basePath={basePath}
level={level + 1}
/>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,36 @@
"use client";
import Link from "next/link";
import { Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
import { Badge } from "@sase/ui";
interface VehicleCardProps {
id: string;
vin: string;
brandName: string;
model: string;
year?: number | string | null;
href?: string;
}
export function VehicleCard({ id, vin, brandName, model, year, href }: VehicleCardProps) {
const linkHref = href || `/dashboard/vehicles/${id}`;
return (
<Link href={linkHref}>
<Card className="cursor-pointer transition-shadow hover:shadow-md">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base">
{brandName} {model}
</CardTitle>
{year && <Badge variant="secondary">{year}</Badge>}
</div>
</CardHeader>
<CardContent>
<p className="font-mono text-sm text-muted-foreground">{vin}</p>
</CardContent>
</Card>
</Link>
);
}

View File

@@ -0,0 +1,55 @@
"use client";
import { useState } from "react";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Search } from "lucide-react";
import { isValidVin } from "@sase/shared";
interface VinInputProps {
onSubmit: (vin: string) => void;
loading?: boolean;
error?: string | null;
}
export function VinInput({ onSubmit, loading, error }: VinInputProps) {
const [vin, setVin] = useState("");
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const cleanVin = vin.toUpperCase().trim();
if (!isValidVin(cleanVin)) return;
onSubmit(cleanVin);
}
const vinUpper = vin.toUpperCase().trim();
const isInvalid = vinUpper.length === 17 && !isValidVin(vinUpper);
return (
<div>
<form onSubmit={handleSubmit} className="flex gap-3">
<Input
placeholder="VIN numarasini girin (17 karakter)"
value={vin}
onChange={(e) => setVin(e.target.value.toUpperCase())}
maxLength={17}
className="font-mono text-lg tracking-wider"
/>
<Button type="submit" disabled={loading || vin.length !== 17 || isInvalid}>
{loading ? (
<span className="animate-spin">...</span>
) : (
<Search className="h-4 w-4" />
)}
Ara
</Button>
</form>
{isInvalid && (
<p className="mt-2 text-sm text-destructive">
Gecersiz VIN. 17 karakter olmali, I, O, Q harfleri kullanilamaz.
</p>
)}
{error && <p className="mt-2 text-sm text-destructive">{error}</p>}
</div>
);
}

View File

@@ -1,4 +1,4 @@
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000/api";
const API_URL = "/api";
type RequestOptions = {
method?: string;

View File

@@ -1,7 +1,7 @@
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_API_URL?.replace("/api", "") || "http://localhost:4000",
baseURL: typeof window !== "undefined" ? window.location.origin : (process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"),
basePath: "/api/auth",
});

View File

@@ -3,7 +3,7 @@ import type { NextRequest } from "next/server";
const publicPaths = ["/", "/pricing", "/login", "/register", "/forgot-password", "/reset-password"];
export function middleware(request: NextRequest) {
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// Allow public paths