admin 권한 세팅
Some checks failed
deploy-on-main / deploy (push) Failing after 21s

This commit is contained in:
koreacomp5
2025-11-10 11:26:00 +09:00
parent 97c8e1c9fb
commit cb2d1f34d3
6 changed files with 65 additions and 11 deletions

View File

@@ -225,7 +225,7 @@ model Post {
// 사용자 // 사용자
model User { model User {
userId String @id @default(cuid()) userId String @id
nickname String @unique nickname String @unique
passwordHash String? passwordHash String?
name String name String

View File

@@ -121,16 +121,29 @@ async function createRandomUsers(count = 100) {
const createdUsers = []; const createdUsers = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
// 닉네임을 결정론적으로 생성해 재실행 시 중복 생성 방지 // 고정 ID: user001, user002, ...
const nickname = `user${String(i + 1).padStart(3, "0")}`; const userId = `user${String(i + 1).padStart(3, "0")}`;
const existing = await prisma.user.findUnique({ where: { nickname } }); const existing = await prisma.user.findUnique({ where: { userId } });
let user = existing; let user = existing;
if (!existing) { if (!existing) {
const name = generateRandomKoreanName(); const name = generateRandomKoreanName();
const birth = randomDate(1975, 2005); const birth = randomDate(1975, 2005);
const phone = await findAvailablePhone(i + 2); // admin이 0001 사용하므로 겹치지 않도록 오프셋 const phone = await findAvailablePhone(i + 2); // admin이 0001 사용하므로 겹치지 않도록 오프셋
// 닉네임: 중복 없는 랜덤 한글 생성
let nickname = generateRandomKoreanName();
for (let tries = 0; tries < 10; tries++) {
const dup = await prisma.user.findUnique({ where: { nickname } });
if (!dup) break;
nickname = generateRandomKoreanName();
}
// 그래도 중복이면 희귀 조합 한 번 더 시도
const finalDup = await prisma.user.findUnique({ where: { nickname } });
if (finalDup) {
nickname = generateRandomKoreanName();
}
user = await prisma.user.create({ user = await prisma.user.create({
data: { data: {
userId,
nickname, nickname,
name, name,
birth, birth,
@@ -247,6 +260,7 @@ async function upsertAdmin() {
level: 200, level: 200,
}, },
create: { create: {
userId: "admin",
nickname: "admin", nickname: "admin",
name: "Administrator", name: "Administrator",
birth: new Date("1990-01-01"), birth: new Date("1990-01-01"),

View File

@@ -47,6 +47,7 @@ export default function AdminUsersPage() {
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead className="bg-[#f6f4f4] border-b border-[#e6e6e6]"> <thead className="bg-[#f6f4f4] border-b border-[#e6e6e6]">
<tr> <tr>
<th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]">ID</th>
<th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]"></th> <th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]"></th>
<th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]"></th> <th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]"></th>
<th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]"></th> <th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]"></th>
@@ -90,6 +91,7 @@ function Row({ u, onChanged }: { u: any; onChanged: () => void }) {
const allRoles = ["admin", "editor", "user"] as const; const allRoles = ["admin", "editor", "user"] as const;
return ( return (
<tr className="hover:bg-neutral-50"> <tr className="hover:bg-neutral-50">
<td className="px-4 py-2 text-left tabular-nums">{u.userId}</td>
<td className="px-4 py-2 text-left">{u.nickname}</td> <td className="px-4 py-2 text-left">{u.nickname}</td>
<td className="px-4 py-2 text-left">{u.name}</td> <td className="px-4 py-2 text-left">{u.name}</td>
<td className="px-4 py-2 text-left">{u.phone}</td> <td className="px-4 py-2 text-left">{u.phone}</td>

View File

@@ -148,7 +148,7 @@ export default function HorizontalCardScroller({ items }: HorizontalCardScroller
</div> </div>
</div> </div>
<div className="pointer-events-none absolute bottom-[-5px] left-1/2 -translate-x-1/2 z-20 flex items-center gap-6"> <div className="pointer-events-none absolute bottom-[-5px] left-1/2 -translate-x-1/2 z-20 flex items-center gap-6 w-full">
<button <button
type="button" type="button"
aria-label="이전" aria-label="이전"
@@ -159,7 +159,7 @@ export default function HorizontalCardScroller({ items }: HorizontalCardScroller
<path d="M9.5 5.5L15 12l-5.5 6.5" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" /> <path d="M9.5 5.5L15 12l-5.5 6.5" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</button> </button>
<div ref={trackRef} className="pointer-events-auto relative h-1 w-[480px] rounded bg-[#EDEDED]"> <div ref={trackRef} className="pointer-events-auto relative h-1 flex-1 min-w-0 max-w-[480px] rounded bg-[#EDEDED]">
<div <div
className="absolute top-0 h-1 rounded bg-[var(--red-50,#F94B37)]" className="absolute top-0 h-1 rounded bg-[var(--red-50,#F94B37)]"
style={{ width: `${thumbWidth}px`, left: `${thumbLeft}px` }} style={{ width: `${thumbWidth}px`, left: `${thumbLeft}px` }}

View File

@@ -43,7 +43,7 @@ export function SendMessageForm({ receiverId, receiverNickname, onSent }: { rece
return ( return (
<form onSubmit={onSubmit} className="flex flex-col gap-2"> <form onSubmit={onSubmit} className="flex flex-col gap-2">
<div className="text-sm text-neutral-700"> <div className="text-sm text-neutral-700">
: <span className="font-semibold">{receiverNickname || receiverId}</span> : <span className="font-semibold">{receiverNickname || "알 수 없음"}</span>
</div> </div>
<textarea <textarea
value={body} value={body}

View File

@@ -11,13 +11,51 @@ export async function middleware(req: NextRequest) {
const uid = req.cookies.get("uid")?.value; const uid = req.cookies.get("uid")?.value;
const isAdmin = req.cookies.get("isAdmin")?.value; const isAdmin = req.cookies.get("isAdmin")?.value;
// 세션 검증 엔드포인트 자체에 대해선 미들웨어 검증을 생략 (무한 루프 방지)
if (pathname === "/api/auth/session") {
return response;
}
// uid 쿠키가 있어도 실제 유저가 존재하지 않으면 비로그인으로 간주하고 쿠키 정리
let authenticated = false;
if (uid) {
try {
const verifyUrl = new URL("/api/auth/session", req.url);
const verifyRes = await fetch(verifyUrl.toString(), {
headers: {
// 현재 요청의 쿠키를 전달하여 세션을 검증
cookie: req.headers.get("cookie") ?? "",
},
});
if (verifyRes.ok) {
const data = await verifyRes.json().catch(() => ({ ok: false }));
authenticated = !!data?.ok;
}
} catch {
authenticated = false;
}
// 유효하지 않은 uid 쿠키는 즉시 제거
if (!authenticated) {
response.cookies.set("uid", "", { path: "/", maxAge: 0 });
response.cookies.set("isAdmin", "", { path: "/", maxAge: 0 });
}
}
// Admin 페이지 보호 // Admin 페이지 보호
if (pathname.startsWith("/admin")) { if (pathname.startsWith("/admin")) {
console.log("Admin 페이지 접근");
console.log("uid", uid);
// 비로그인 사용자는 로그인 페이지로 // 비로그인 사용자는 로그인 페이지로
if (!uid) { if (!authenticated) {
const loginUrl = req.nextUrl.clone(); const loginUrl = req.nextUrl.clone();
loginUrl.pathname = "/login"; loginUrl.pathname = "/login";
return NextResponse.redirect(loginUrl); const res = NextResponse.redirect(loginUrl);
// 리다이렉트 응답에도 쿠키 정리 반영
if (uid && !authenticated) {
res.cookies.set("uid", "", { path: "/", maxAge: 0 });
res.cookies.set("isAdmin", "", { path: "/", maxAge: 0 });
}
return res;
} }
// 로그인했지만 관리자가 아니면 홈으로 // 로그인했지만 관리자가 아니면 홈으로
if (isAdmin !== "1") { if (isAdmin !== "1") {
@@ -29,7 +67,7 @@ export async function middleware(req: NextRequest) {
// 로그인된 상태에서 로그인 페이지 접근 시 홈으로 리다이렉트 // 로그인된 상태에서 로그인 페이지 접근 시 홈으로 리다이렉트
if (pathname === "/login" || pathname.startsWith("/login/")) { if (pathname === "/login" || pathname.startsWith("/login/")) {
if (uid) { if (authenticated) {
const homeUrl = req.nextUrl.clone(); const homeUrl = req.nextUrl.clone();
homeUrl.pathname = "/"; homeUrl.pathname = "/";
return NextResponse.redirect(homeUrl); return NextResponse.redirect(homeUrl);
@@ -37,7 +75,7 @@ export async function middleware(req: NextRequest) {
} }
const needAuth = protectedApi.some((re) => re.test(pathname)); const needAuth = protectedApi.some((re) => re.test(pathname));
if (needAuth && !uid) { if (needAuth && !authenticated) {
return new NextResponse(JSON.stringify({ error: "Unauthorized" }), { status: 401 }); return new NextResponse(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
} }