first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:11:03 +03:00
commit 031582ea2c
98 changed files with 13281 additions and 0 deletions

214
app/lib/api-auth.ts Normal file
View File

@@ -0,0 +1,214 @@
import { NextRequest } from "next/server";
import { db } from "@/db";
import { apiKeys, user } from "@/db/schema";
import { eq, and } from "drizzle-orm";
import { verifyJWT, isValidAPIKeyFormat } from "./jwt";
import { UserRole } from "./permissions";
import { auth } from "@/app/lib/auth";
export interface AuthenticatedRequest extends NextRequest {
userId?: string;
email?: string;
role?: UserRole;
}
export interface AuthResult {
authenticated: boolean;
userId?: string;
email?: string;
role?: UserRole;
error?: string;
}
/**
* API isteklerini doğrula (JWT token veya API key ile)
*
* Kullanım:
* const authResult = await authenticateAPIRequest(request);
* if (!authResult.authenticated) {
* return NextResponse.json({ error: authResult.error }, { status: 401 });
* }
* const userId = authResult.userId;
*/
/**
* Cookie oturumu (Better Auth) veya Bearer (JWT / API key) ile doğrula.
* Web arayüzünden yapılan isteklerde session; script/istemci için Authorization kullanılır.
*/
export async function authenticateWebOrAPIRequest(
request: NextRequest
): Promise<AuthResult> {
const session = await auth.api.getSession({
headers: request.headers,
});
if (session?.user) {
const u = session.user as { id: string };
try {
const users = await db
.select()
.from(user)
.where(eq(user.id, u.id))
.limit(1);
if (users.length === 0) {
return { authenticated: false, error: "Kullanıcı bulunamadı." };
}
const userData = users[0];
return {
authenticated: true,
userId: u.id,
email: userData.email,
role: (userData.role as UserRole) || "user",
};
} catch (error) {
console.error("Session user lookup error:", error);
return {
authenticated: false,
error: "Kimlik doğrulama sırasında bir hata oluştu.",
};
}
}
return authenticateAPIRequest(request);
}
export async function authenticateAPIRequest(request: NextRequest): Promise<AuthResult> {
const authHeader = request.headers.get("authorization");
if (!authHeader) {
return {
authenticated: false,
error: "Authorization header eksik. Bearer token veya API key gerekli.",
};
}
// Bearer token kontrolü
if (authHeader.startsWith("Bearer ")) {
const token = authHeader.substring(7);
// JWT token mu yoksa API key mi?
if (isValidAPIKeyFormat(token)) {
// API Key doğrulama
return await validateAPIKey(token);
} else {
// JWT token doğrulama
return await validateJWTToken(token);
}
}
return {
authenticated: false,
error: "Geçersiz authorization formatı. 'Bearer <token>' formatında olmalı.",
};
}
/**
* JWT token doğrula ve kullanıcı bilgilerini getir
*/
async function validateJWTToken(token: string): Promise<AuthResult> {
const payload = verifyJWT(token);
if (!payload) {
return {
authenticated: false,
error: "Geçersiz veya süresi dolmuş token.",
};
}
// Kullanıcı bilgilerini DB'den al (role için)
try {
const users = await db
.select()
.from(user)
.where(eq(user.id, payload.userId))
.limit(1);
if (users.length === 0) {
return {
authenticated: false,
error: "Kullanıcı bulunamadı.",
};
}
const userData = users[0];
return {
authenticated: true,
userId: payload.userId,
email: payload.email,
role: (userData.role as UserRole) || "user",
};
} catch (error) {
console.error("User lookup error:", error);
return {
authenticated: false,
error: "Kimlik doğrulama sırasında bir hata oluştu.",
};
}
}
/**
* API key doğrula (veritabanından kontrol)
*/
async function validateAPIKey(key: string): Promise<AuthResult> {
try {
const apiKey = await db
.select()
.from(apiKeys)
.where(and(eq(apiKeys.key, key), eq(apiKeys.isActive, true)))
.limit(1);
if (apiKey.length === 0) {
return {
authenticated: false,
error: "Geçersiz API key.",
};
}
const keyData = apiKey[0];
// Süre kontrolü
if (keyData.expiresAt && keyData.expiresAt < new Date()) {
return {
authenticated: false,
error: "API key süresi dolmuş.",
};
}
// Kullanıcı bilgilerini al
const users = await db
.select()
.from(user)
.where(eq(user.id, keyData.userId))
.limit(1);
if (users.length === 0) {
return {
authenticated: false,
error: "Kullanıcı bulunamadı.",
};
}
const userData = users[0];
// Son kullanım tarihini güncelle (opsiyonel)
await db
.update(apiKeys)
.set({ lastUsedAt: new Date() })
.where(eq(apiKeys.id, keyData.id));
return {
authenticated: true,
userId: keyData.userId,
email: userData.email,
role: (userData.role as UserRole) || "user",
};
} catch (error) {
console.error("API key doğrulama hatası:", error);
return {
authenticated: false,
error: "Kimlik doğrulama sırasında bir hata oluştu.",
};
}
}

51
app/lib/api-key-utils.ts Normal file
View File

@@ -0,0 +1,51 @@
/** Kullanıcı/admin API key oluşturma ve güncelleme için ortak süre kuralları */
export const MAX_API_KEY_NAME_LEN = 120;
export const MAX_EXPIRES_DAYS = 3650;
export function expiresAtFromDays(days: number): Date {
return new Date(Date.now() + days * 86_400_000);
}
/**
* Body'den süre çıkarır: yok/null/0 = süresiz; 1..MAX_EXPIRES_DAYS = o kadar gün.
* Geçersiz sayıda null döner (çağıran 400 verebilir).
*/
export function parseExpiresInDaysOptional(
raw: unknown
): { ok: true; value: number | null } | { ok: false; error: string } {
if (raw === undefined || raw === null) {
return { ok: true, value: null };
}
if (typeof raw !== "number" || !Number.isFinite(raw)) {
return { ok: false, error: "expiresInDays sayı olmalıdır." };
}
const d = Math.floor(raw);
if (d === 0) {
return { ok: true, value: null };
}
if (d < 1 || d > MAX_EXPIRES_DAYS) {
return {
ok: false,
error: `expiresInDays 0 (süresiz) veya 1${MAX_EXPIRES_DAYS} arası olmalıdır.`,
};
}
return { ok: true, value: d };
}
/** Süresiz: daysRemaining null. Süreli: kalan tam gün sayısı (bitiş anına kadar; dolmuşsa 0). */
export function getDaysRemaining(expiresAt: Date | null | undefined): number | null {
if (expiresAt == null) return null;
const ms = expiresAt.getTime() - Date.now();
if (ms <= 0) return 0;
return Math.ceil(ms / 86_400_000);
}
/** Kullanıcı ve admin arayüzleri için kısa Türkçe ibare */
export function getExpiryRemainingLabel(expiresAt: Date | null | undefined): string {
if (expiresAt == null) return "Süresiz";
const d = getDaysRemaining(expiresAt);
if (d === 0) return "Süresi doldu";
if (d === 1) return "1 gün kaldı";
return `${d} gün kaldı`;
}

37
app/lib/auth.ts Normal file
View File

@@ -0,0 +1,37 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db";
import * as schema from "@/db/schema";
// Validate BETTER_AUTH_SECRET at runtime (not during build)
const secret = process.env.BETTER_AUTH_SECRET;
if (!secret && process.env.NODE_ENV === "production") {
console.warn("WARNING: BETTER_AUTH_SECRET is not set. Authentication will not work properly.");
}
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: {
user: schema.user,
session: schema.session,
account: schema.account,
verification: schema.verification,
},
}),
emailAndPassword: {
enabled: true,
},
secret: secret || "build-time-secret-key-minimum-32-characters-long-temp",
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
user: {
additionalFields: {
role: {
type: "string",
defaultValue: "user",
required: false,
input: false, // Don't allow setting role on signup
},
},
},
});

54
app/lib/jwt.ts Normal file
View File

@@ -0,0 +1,54 @@
import jwt, { SignOptions } from "jsonwebtoken";
import { nanoid } from "nanoid";
const JWT_SECRET = process.env.JWT_SECRET || process.env.BETTER_AUTH_SECRET || "fallback-secret-key";
const API_KEY_PREFIX = "img_";
export interface JWTPayload {
userId: string;
email: string;
type: "access" | "refresh";
}
/**
* JWT token oluştur
* @param payload - Token içeriği
* @param expiresIn - Geçerlilik süresi (örn: "7d", "1h")
*/
export function signJWT(payload: JWTPayload, expiresIn: string | number = "7d"): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn } as SignOptions);
}
/**
* JWT token doğrula
* @param token - Doğrulanacak token
*/
export function verifyJWT(token: string): JWTPayload | null {
try {
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
return decoded;
} catch (error) {
return null;
}
}
/**
* API key oluştur
* Formad: img_xxxxxxxxxxxxxxxxxxxxxxxx
*/
export function generateAPIKey(): string {
return `${API_KEY_PREFIX}${nanoid(32)}`;
}
/**
* API key validasyonu
*/
export function isValidAPIKeyFormat(key: string): boolean {
return key.startsWith(API_KEY_PREFIX) && key.length === 36; // img_ + 32 chars
}
/** Liste/detay için tam anahtarı göstermez (img_xxxx…yyyy) */
export function maskApiKey(key: string): string {
if (key.length < 12) return "img_••••";
return `${key.slice(0, 7)}${key.slice(-4)}`;
}

View File

@@ -0,0 +1,15 @@
next js beter auth yuklu ve drizze orm yuklu posgrsql veritabanı ile entegre edilecek.
drizzle orm ile veritabanına bağlanılacak.
beter auth ile register yapılacak.
beter auth ile giriş yapılacak.
giriş yapıldıktan sonra kullanıcının bilgileri veritabanından alınacak.
kullanıcının bilgileri veritabanından alındıktan sonra kullanıcının bilgileri sayfada görüntülenecek.
birkaç yapilandirma eklendi ama duzgun olmayabilir sen kotrol et ve duzelt
sadece login olmus userlerin giris yapabilecegi bir sayfa olacak. ve sayfada resim dosyalri yuklenecek en boy kalite format vs kullnacini verdigi bilgilere gore
resim manipule edilecek ve database ye drizzle orm ile kaydedilecek.
resim url si çıkartilarak download edilebilir ve bir buton ile resmin url si kopyalanabilir. olacak ve bu url ile resim indirilebilir.

93
app/lib/permissions.ts Normal file
View File

@@ -0,0 +1,93 @@
import { db } from "@/db";
import { user } from "@/db/schema";
import { eq } from "drizzle-orm";
export type UserRole = "user" | "admin" | "moderator";
// Permission tanımları
export const PERMISSIONS = {
// Image permissions
IMAGE_UPLOAD: "image:upload",
IMAGE_DELETE_OWN: "image:delete:own",
IMAGE_DELETE_ANY: "image:delete:any",
IMAGE_VIEW_OWN: "image:view:own",
IMAGE_VIEW_ANY: "image:view:any",
// User permissions
USER_VIEW: "user:view",
USER_EDIT: "user:edit",
USER_DELETE: "user:delete",
USER_MANAGE_ROLES: "user:manage:roles",
} as const;
// Role'lere göre izinler
export const ROLE_PERMISSIONS: Record<UserRole, string[]> = {
user: [
PERMISSIONS.IMAGE_UPLOAD,
PERMISSIONS.IMAGE_DELETE_OWN,
PERMISSIONS.IMAGE_VIEW_OWN,
],
moderator: [
PERMISSIONS.IMAGE_UPLOAD,
PERMISSIONS.IMAGE_DELETE_OWN,
PERMISSIONS.IMAGE_VIEW_OWN,
PERMISSIONS.IMAGE_VIEW_ANY,
PERMISSIONS.USER_VIEW,
],
admin: Object.values(PERMISSIONS), // Tüm izinler
};
/**
* Kullanıcının belirli bir role sahip olup olmadığını kontrol eder
*/
export function hasRole(userRole: UserRole, requiredRole: UserRole | UserRole[]): boolean {
const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole];
return roles.includes(userRole);
}
/**
* Kullanıcının belirli bir izne sahip olup olmadığını kontrol eder
*/
export function hasPermission(userRole: UserRole, permission: string): boolean {
const rolePermissions = ROLE_PERMISSIONS[userRole] || [];
return rolePermissions.includes(permission);
}
/**
* Kullanıcının birden fazla izne sahip olup olmadığını kontrol eder
*/
export function hasPermissions(userRole: UserRole, permissions: string[]): boolean {
return permissions.every(permission => hasPermission(userRole, permission));
}
/**
* Kullanıcının en az bir izne sahip olup olmadığını kontrol eder
*/
export function hasAnyPermission(userRole: UserRole, permissions: string[]): boolean {
return permissions.some(permission => hasPermission(userRole, permission));
}
/**
* Kullanıcının admin olup olmadığını kontrol eder
*/
export function isAdmin(userRole: UserRole): boolean {
return userRole === "admin";
}
/**
* Kullanıcı bilgilerini userId'den alır
*/
export async function getUserById(userId: string) {
const users = await db.select().from(user).where(eq(user.id, userId)).limit(1);
return users[0] || null;
}
/**
* Kullanıcının rolünü günceller (sadece admin yapabilir)
*/
export async function updateUserRole(userId: string, newRole: UserRole) {
await db.update(user).set({
role: newRole,
updatedAt: new Date()
}).where(eq(user.id, userId));
}

81
app/lib/r2-storage.ts Normal file
View File

@@ -0,0 +1,81 @@
import { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
// R2 configuration
const R2_ACCOUNT_ID = process.env.R2_ACCOUNT_ID || "";
const R2_ACCESS_KEY_ID = process.env.R2_ACCESS_KEY_ID || "";
const R2_SECRET_ACCESS_KEY = process.env.R2_SECRET_ACCESS_KEY || "";
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME || "";
const R2_PUBLIC_URL = process.env.R2_PUBLIC_URL || "";
// S3 client configuration for Cloudflare R2
const s3Client = new S3Client({
region: "auto",
endpoint: `https://${R2_ACCOUNT_ID}.eu.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: R2_ACCESS_KEY_ID,
secretAccessKey: R2_SECRET_ACCESS_KEY,
},
});
export interface UploadOptions {
buffer: Buffer;
fileName: string;
contentType: string;
}
/**
* Upload a file to R2
*/
export async function uploadToR2(options: UploadOptions): Promise<string> {
const { buffer, fileName, contentType } = options;
try {
const command = new PutObjectCommand({
Bucket: R2_BUCKET_NAME,
Key: fileName,
Body: buffer,
ContentType: contentType,
});
await s3Client.send(command);
// Return the public URL
return `${R2_PUBLIC_URL}/${fileName}`;
} catch (error) {
console.error("R2 upload error:", error);
throw new Error(`R2'ye yükleme başarısız: ${error}`);
}
}
/**
* Delete a file from R2
*/
export async function deleteFromR2(fileName: string): Promise<void> {
try {
const command = new DeleteObjectCommand({
Bucket: R2_BUCKET_NAME,
Key: fileName,
});
await s3Client.send(command);
} catch (error) {
console.error("R2 delete error:", error);
throw new Error(`R2'den silme başarısız: ${error}`);
}
}
/**
* Get content type from file extension
*/
export function getContentType(format: string): string {
const contentTypeMap: Record<string, string> = {
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
gif: "image/gif",
webp: "image/webp",
avif: "image/avif",
};
return contentTypeMap[format] || "application/octet-stream";
}