first commit
This commit is contained in:
214
app/lib/api-auth.ts
Normal file
214
app/lib/api-auth.ts
Normal 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
51
app/lib/api-key-utils.ts
Normal 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
37
app/lib/auth.ts
Normal 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
54
app/lib/jwt.ts
Normal 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)}`;
|
||||
}
|
||||
15
app/lib/next js beter auth yuklu ve drizze orm y.txt
Normal file
15
app/lib/next js beter auth yuklu ve drizze orm y.txt
Normal 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
93
app/lib/permissions.ts
Normal 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
81
app/lib/r2-storage.ts
Normal 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";
|
||||
}
|
||||
Reference in New Issue
Block a user