feat: add admin panel, Openinary media integration and standardized footer backlink
This commit is contained in:
+68
@@ -0,0 +1,68 @@
|
||||
import { SignJWT, jwtVerify } from 'jose';
|
||||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const secretKey = process.env.JWT_SECRET || 'super-secret-key-replace-me-in-production';
|
||||
const key = new TextEncoder().encode(secretKey);
|
||||
|
||||
export async function encrypt(payload: any) {
|
||||
return await new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('24h')
|
||||
.sign(key);
|
||||
}
|
||||
|
||||
export async function decrypt(input: string): Promise<any> {
|
||||
try {
|
||||
const { payload } = await jwtVerify(input, key, {
|
||||
algorithms: ['HS256'],
|
||||
});
|
||||
return payload;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSession() {
|
||||
const cookieStore = await cookies();
|
||||
const session = cookieStore.get('session')?.value;
|
||||
if (!session) return null;
|
||||
return await decrypt(session);
|
||||
}
|
||||
|
||||
export async function createSession(userId: string) {
|
||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
const session = await encrypt({ userId, expires });
|
||||
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set('session', session, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
expires: expires,
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteSession() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete('session');
|
||||
}
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
const session = request.cookies.get('session')?.value;
|
||||
if (!session) return;
|
||||
|
||||
const parsed = await decrypt(session);
|
||||
if (!parsed) return;
|
||||
|
||||
parsed.expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
const res = NextResponse.next();
|
||||
res.cookies.set({
|
||||
name: 'session',
|
||||
value: await encrypt(parsed),
|
||||
httpOnly: true,
|
||||
expires: parsed.expires,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
export default function openinaryLoader({ src, width, quality }: { src: string, width: number, quality?: number }) {
|
||||
// Handle already absolute openinary URLs
|
||||
let path = src;
|
||||
if (src.startsWith('https://media.ayris.tech/t/')) {
|
||||
// format: https://media.ayris.tech/t/w_800,h_800/moyqr/img.jpg
|
||||
const parts = src.split('/');
|
||||
path = parts.slice(5).join('/');
|
||||
} else if (src.startsWith('https://media.ayris.tech/upload/')) {
|
||||
// format: https://media.ayris.tech/upload/moyqr/img.jpg
|
||||
const parts = src.split('/');
|
||||
path = parts.slice(4).join('/');
|
||||
} else if (src.includes('res.cloudinary.com')) {
|
||||
// Correctly apply width & quality for unmigrated Cloudinary URLs
|
||||
// e.g. https://res.cloudinary.com/domain/image/upload/v1234/path.jpg
|
||||
// becomes: https://res.cloudinary.com/domain/image/upload/w_800,f_webp,q_75/v1234/path.jpg
|
||||
const parts = src.split('/upload/');
|
||||
if (parts.length === 2) {
|
||||
return `${parts[0]}/upload/w_${width},f_webp,q_${quality || 75}/${parts[1]}`;
|
||||
}
|
||||
return src;
|
||||
} else if (src.startsWith('http')) {
|
||||
// For other external URLs, we append the width as a query parameter to satisfy Next.js.
|
||||
const url = new URL(src);
|
||||
url.searchParams.set('w', width.toString());
|
||||
if (quality) {
|
||||
url.searchParams.set('q', quality.toString());
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// Clean up any leading slash
|
||||
if (path.startsWith('/')) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
|
||||
return `https://media.ayris.tech/t/w_${width},f_webp,q_${quality || 75}/${path}`
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const BASE = process.env.NEXT_PUBLIC_OPENINARY_URL;
|
||||
|
||||
export function optimizedImage(path: string, params: string) {
|
||||
return `${BASE}/t/${params}/${path}`;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export async function uploadToOpeninary(file: File, folder: string) {
|
||||
const formData = new FormData();
|
||||
formData.append("files", file);
|
||||
formData.append("folder", folder);
|
||||
|
||||
const res = await fetch(`${process.env.OPENINARY_API_URL}/api/upload`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${process.env.OPENINARY_API_KEY}` },
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
console.error("Openinary upload error:", errorText);
|
||||
throw new Error("Upload başarısız: " + errorText);
|
||||
}
|
||||
const data = await res.json();
|
||||
return data.files[0]; // { path, url, size, ... }
|
||||
}
|
||||
Reference in New Issue
Block a user