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:
@@ -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" }),
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
108
apps/web/src/components/categories/category-tree.tsx
Normal file
108
apps/web/src/components/categories/category-tree.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
36
apps/web/src/components/vehicles/vehicle-card.tsx
Normal file
36
apps/web/src/components/vehicles/vehicle-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
55
apps/web/src/components/vehicles/vin-input.tsx
Normal file
55
apps/web/src/components/vehicles/vin-input.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user