diff --git a/src/app/admin/login/page.tsx b/src/app/admin/login/page.tsx
new file mode 100644
index 0000000..34e47f0
--- /dev/null
+++ b/src/app/admin/login/page.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+
+export default function AdminLoginPage() {
+ const router = useRouter();
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ setError("");
+ try {
+ const res = await fetch("/api/admin/login", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ username, password }),
+ });
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || "Најавата не успеа");
+ }
+ router.push("/admin/dashboard");
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Најавата не успеа");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/api/admin/change-password/route.ts b/src/app/api/admin/change-password/route.ts
new file mode 100644
index 0000000..1d6ebf3
--- /dev/null
+++ b/src/app/api/admin/change-password/route.ts
@@ -0,0 +1,45 @@
+import { NextRequest, NextResponse } from "next/server";
+import { hash, compare } from "bcryptjs";
+import { prisma } from "@/lib/prisma";
+import { getAdminSession } from "@/lib/admin-session";
+
+export async function POST(req: NextRequest) {
+ try {
+ const session = await getAdminSession();
+ if (!session) {
+ return NextResponse.json({ error: "Неавторизирано" }, { status: 401 });
+ }
+ if (session.username === "super") {
+ return NextResponse.json({ error: "SuperAdmin не може да ја промени лозинката преку овој метод" }, { status: 400 });
+ }
+
+ const { currentPassword, newPassword } = await req.json();
+ if (!currentPassword || !newPassword) {
+ return NextResponse.json({ error: "Тековната и новата лозинка се задолжителни" }, { status: 400 });
+ }
+ if (newPassword.length < 6) {
+ return NextResponse.json({ error: "Новата лозинка мора да има најмалку 6 карактери" }, { status: 400 });
+ }
+
+ const admin = await prisma.adminUser.findUnique({ where: { username: session.username } });
+ if (!admin) {
+ return NextResponse.json({ error: "Администраторот не е пронајден" }, { status: 404 });
+ }
+
+ const valid = await compare(currentPassword, admin.passwordHash);
+ if (!valid) {
+ return NextResponse.json({ error: "Тековната лозинка е неточна" }, { status: 401 });
+ }
+
+ const passwordHash = await hash(newPassword, 12);
+ await prisma.adminUser.update({
+ where: { username: session.username },
+ data: { passwordHash },
+ });
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error("Change password error:", error);
+ return NextResponse.json({ error: "Внатрешна грешка на серверот" }, { status: 500 });
+ }
+}
diff --git a/src/app/api/admin/login/route.ts b/src/app/api/admin/login/route.ts
new file mode 100644
index 0000000..eb7f070
--- /dev/null
+++ b/src/app/api/admin/login/route.ts
@@ -0,0 +1,41 @@
+import { NextRequest, NextResponse } from "next/server";
+import { compare } from "bcryptjs";
+import { prisma } from "@/lib/prisma";
+import { createAdminSession, cookieOptions } from "@/lib/admin-session";
+
+const SUPER_USERNAME = "super";
+const SUPER_PASSWORD = "admin";
+
+export async function POST(req: NextRequest) {
+ try {
+ const { username, password } = await req.json();
+ if (!username || !password) {
+ return NextResponse.json({ error: "Корисничко име и лозинка се задолжителни" }, { status: 400 });
+ }
+
+ if (username === SUPER_USERNAME && password === SUPER_PASSWORD) {
+ const session = createAdminSession({ username: SUPER_USERNAME, role: "SUPER_ADMIN" });
+ const res = NextResponse.json({ success: true });
+ res.cookies.set(cookieOptions(session));
+ return res;
+ }
+
+ const admin = await prisma.adminUser.findUnique({ where: { username } });
+ if (!admin) {
+ return NextResponse.json({ error: "Невалидно корисничко име или лозинка" }, { status: 401 });
+ }
+
+ const valid = await compare(password, admin.passwordHash);
+ if (!valid) {
+ return NextResponse.json({ error: "Невалидно корисничко име или лозинка" }, { status: 401 });
+ }
+
+ const session = createAdminSession({ username: admin.username, role: admin.role });
+ const res = NextResponse.json({ success: true });
+ res.cookies.set(cookieOptions(session));
+ return res;
+ } catch (error) {
+ console.error("Admin login error:", error);
+ return NextResponse.json({ error: "Внатрешна грешка на серверот" }, { status: 500 });
+ }
+}
diff --git a/src/app/api/admin/logout/route.ts b/src/app/api/admin/logout/route.ts
new file mode 100644
index 0000000..15c0cb7
--- /dev/null
+++ b/src/app/api/admin/logout/route.ts
@@ -0,0 +1,15 @@
+import { NextResponse } from "next/server";
+
+export async function POST() {
+ const res = NextResponse.json({ success: true });
+ res.cookies.set({
+ name: "admin_session",
+ value: "",
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ path: "/admin",
+ maxAge: 0,
+ });
+ return res;
+}
diff --git a/src/lib/admin-session.ts b/src/lib/admin-session.ts
new file mode 100644
index 0000000..07d2e72
--- /dev/null
+++ b/src/lib/admin-session.ts
@@ -0,0 +1,68 @@
+import { cookies } from "next/headers";
+import { createHmac, timingSafeEqual } from "crypto";
+
+const COOKIE_NAME = "admin_session";
+const SEP = ".";
+
+function getSecret(): string {
+ const secret = process.env.ADMIN_SESSION_SECRET;
+ if (!secret) throw new Error("ADMIN_SESSION_SECRET env var is not set");
+ return secret;
+}
+
+function sign(payload: string): string {
+ const hmac = createHmac("sha256", getSecret());
+ hmac.update(payload);
+ return hmac.digest("base64url");
+}
+
+export interface AdminSession {
+ username: string;
+ role: "SUPER_ADMIN" | "ADMIN";
+}
+
+export function createAdminSession(data: AdminSession): string {
+ const payload = Buffer.from(JSON.stringify(data)).toString("base64url");
+ return payload + SEP + sign(payload);
+}
+
+export function verifyAdminSession(token: string): AdminSession | null {
+ const sepIdx = token.lastIndexOf(SEP);
+ if (sepIdx === -1) return null;
+ const payload = token.slice(0, sepIdx);
+ const sig = token.slice(sepIdx + 1);
+ const expectedSig = sign(payload);
+ const sigBuf = Buffer.from(sig);
+ const expectedBuf = Buffer.from(expectedSig);
+ if (sigBuf.length !== expectedBuf.length) return null;
+ try {
+ if (!timingSafeEqual(sigBuf, expectedBuf)) return null;
+ } catch {
+ return null;
+ }
+ try {
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
+ } catch {
+ return null;
+ }
+}
+
+export async function getAdminSession(): Promise {
+ const store = await cookies();
+ const token = store.get(COOKIE_NAME)?.value;
+ if (!token) return null;
+ return verifyAdminSession(token);
+}
+
+export const COOKIE_NAME_ADMIN = COOKIE_NAME;
+
+export function cookieOptions(value: string) {
+ return {
+ name: COOKIE_NAME,
+ value,
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax" as const,
+ path: "/admin",
+ };
+}
diff --git a/src/middleware.ts b/src/middleware.ts
index 2964296..2c64e73 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,9 +1,14 @@
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
+import { verifyAdminSession, COOKIE_NAME_ADMIN } from "@/lib/admin-session";
const isProtectedRoute = createRouteMatcher(["/dashboard(.*)", "/onboarding(.*)", "/api/publish(.*)", "/api/upload(.*)", "/api/user(.*)"]);
+function isAdminRoute(pathname: string): boolean {
+ return pathname.startsWith("/admin") || pathname.startsWith("/api/admin");
+}
+
function getSubdomain(req: NextRequest): string | null {
const host = req.headers.get("host");
if (!host) return null;
@@ -25,6 +30,22 @@ function getSubdomain(req: NextRequest): string | null {
}
export default clerkMiddleware(async (auth, req: NextRequest) => {
+ if (isAdminRoute(req.nextUrl.pathname)) {
+ if (req.nextUrl.pathname === "/admin/login") {
+ return NextResponse.next();
+ }
+
+ const token = req.cookies.get(COOKIE_NAME_ADMIN)?.value;
+ if (!token || !verifyAdminSession(token)) {
+ if (req.nextUrl.pathname.startsWith("/api/")) {
+ return NextResponse.json({ error: "Неавторизирано" }, { status: 401 });
+ }
+ return NextResponse.redirect(new URL("/admin/login", req.url));
+ }
+
+ return NextResponse.next();
+ }
+
if (isProtectedRoute(req)) {
await auth.protect();
}
@@ -41,4 +62,4 @@ export default clerkMiddleware(async (auth, req: NextRequest) => {
export const config = {
matcher: ["/(api|trpc)(.*)", "/__clerk/:path*", "/((?!_next|api/static|.*\\..*).*)"],
-};
\ No newline at end of file
+};