first commit
This commit is contained in:
105
frontend/lib/api-proxy.ts
Normal file
105
frontend/lib/api-proxy.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { optimizeImage } from "./image-optimizer"
|
||||
import { getToken } from "next-auth/jwt"
|
||||
|
||||
const API_URL = (process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080") + "/api/v1"
|
||||
|
||||
export async function handleImageProxyRequest(req: NextRequest, targetEndpoint: string) {
|
||||
try {
|
||||
const formData = await req.formData()
|
||||
const newFormData = new FormData()
|
||||
|
||||
// Process fields
|
||||
// We need to iterate twice: first to collect all config fields, then to process files
|
||||
const configMap = new Map<string, string>()
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (typeof value === "string") {
|
||||
configMap.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value instanceof File && value.size > 0) {
|
||||
// Determine configuration for this file
|
||||
let width = 0
|
||||
let height = 0
|
||||
let quality = 80
|
||||
let format = "avif"
|
||||
|
||||
// Heuristic 1: "image" field maps to root "width", "height", etc.
|
||||
if (key === "image") {
|
||||
width = Number(configMap.get("width")) || 0
|
||||
height = Number(configMap.get("height")) || 0
|
||||
quality = Number(configMap.get("quality")) || 80
|
||||
format = configMap.get("format") || "avif"
|
||||
}
|
||||
// Heuristic 2: "[prefix]_logo" maps to "[prefix]_width", etc.
|
||||
else if (key.endsWith("_logo")) {
|
||||
const prefix = key.replace("_logo", "")
|
||||
width = Number(configMap.get(`${prefix}_width`)) || 0
|
||||
height = Number(configMap.get(`${prefix}_height`)) || 0
|
||||
quality = Number(configMap.get(`${prefix}_quality`)) || 80
|
||||
format = configMap.get(`${prefix}_format`) || "avif"
|
||||
}
|
||||
|
||||
// If any config is found (or defaults for "image"), optimize
|
||||
if (key === "image" || key.endsWith("_logo")) {
|
||||
const buffer = Buffer.from(await value.arrayBuffer())
|
||||
const { buffer: processedBuffer, contentType, filename } = await optimizeImage(buffer, {
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
format,
|
||||
})
|
||||
|
||||
// Create Blob from Buffer
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const blob = new Blob([processedBuffer as any], { type: contentType })
|
||||
newFormData.append(key, blob, filename)
|
||||
} else {
|
||||
// Just forward other files as is
|
||||
newFormData.append(key, value)
|
||||
}
|
||||
} else {
|
||||
newFormData.append(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Get Auth Token (Server-side)
|
||||
// NextAuth's getSession might not work in API routes depending on setup,
|
||||
// getToken is more reliable for middleware/API routes.
|
||||
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
|
||||
|
||||
const headers: HeadersInit = {}
|
||||
if (token && token.accessToken) {
|
||||
headers["Authorization"] = `Bearer ${token.accessToken}`
|
||||
}
|
||||
|
||||
// Determine method (POST or PUT)
|
||||
const method = req.method
|
||||
|
||||
const targetUrl = `${API_URL}${targetEndpoint}`
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: newFormData,
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(data)
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error("Proxy Error:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : "Internal Server Error"
|
||||
return NextResponse.json(
|
||||
{ error: errorMessage },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
21
frontend/lib/auth-schema.ts
Normal file
21
frontend/lib/auth-schema.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.string().email({ message: "Geçerli bir e-posta adresi giriniz." }),
|
||||
password: z.string().min(6, { message: "Şifre en az 6 karakter olmalıdır." }),
|
||||
// Turnstile token is optional in schema but required for submission logic if enabled
|
||||
turnstileToken: z.string().optional(),
|
||||
});
|
||||
|
||||
export const registerSchema = z.object({
|
||||
username: z.string().min(3, { message: "Kullanıcı adı en az 3 karakter olmalıdır." }),
|
||||
email: z.string().email({ message: "Geçerli bir e-posta adresi giriniz." }),
|
||||
password: z.string().min(8, { message: "Şifre en az 8 karakter olmalıdır." }),
|
||||
confirmPassword: z.string().min(8, { message: "Şifre tekrarı en az 8 karakter olmalıdır." }),
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Şifreler eşleşmiyor.",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
export type LoginInput = z.infer<typeof loginSchema>;
|
||||
export type RegisterInput = z.infer<typeof registerSchema>;
|
||||
54
frontend/lib/image-optimizer.ts
Normal file
54
frontend/lib/image-optimizer.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import sharp from "sharp"
|
||||
|
||||
export interface ImageOptions {
|
||||
width?: number
|
||||
height?: number
|
||||
quality?: number
|
||||
format?: string // "webp", "jpeg", "png", "avif"
|
||||
}
|
||||
|
||||
export async function optimizeImage(buffer: Buffer, options: ImageOptions): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
|
||||
let pipeline = sharp(buffer)
|
||||
|
||||
// Resize if width or height is provided and greater than 0
|
||||
if ((options.width && options.width > 0) || (options.height && options.height > 0)) {
|
||||
pipeline = pipeline.resize({
|
||||
width: options.width && options.width > 0 ? options.width : undefined,
|
||||
height: options.height && options.height > 0 ? options.height : undefined,
|
||||
fit: "cover", // Or 'contain', 'fill' based on requirement. Cover is usually good for heroes.
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Default format is AVIF if not specified
|
||||
const format = options.format?.toLowerCase() || "avif"
|
||||
const quality = options.quality && options.quality > 0 ? options.quality : 80
|
||||
|
||||
switch (format) {
|
||||
case "jpeg":
|
||||
case "jpg":
|
||||
pipeline = pipeline.jpeg({ quality })
|
||||
break
|
||||
case "png":
|
||||
pipeline = pipeline.png({ quality, compressionLevel: 9 }) // PNG quality is different, usually compression level
|
||||
break
|
||||
case "webp":
|
||||
pipeline = pipeline.webp({ quality })
|
||||
break
|
||||
case "avif":
|
||||
pipeline = pipeline.avif({ quality })
|
||||
break
|
||||
default:
|
||||
pipeline = pipeline.avif({ quality })
|
||||
break
|
||||
}
|
||||
|
||||
const processedBuffer = await pipeline.toBuffer()
|
||||
const extension = format === "jpg" ? "jpeg" : format
|
||||
|
||||
return {
|
||||
buffer: processedBuffer,
|
||||
contentType: `image/${extension}`,
|
||||
filename: `image.${extension}`, // Generic filename, caller can prepend/append if needed
|
||||
}
|
||||
}
|
||||
6
frontend/lib/utils.ts
Normal file
6
frontend/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user