사용자 로그인, 가입 처리
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
|
||||
import { verifyPassword, signAccessToken } from "@/features/auth/authCrypto";
|
||||
|
||||
// /api/auth/login
|
||||
// - 이메일/비밀번호를 검증해 로그인시키고,
|
||||
// - JWT 액세스 토큰을 httpOnly 쿠키에 설정한 뒤, 비밀번호 해시를 제외한 유저 정보를 반환한다.
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json().catch(() => null as any);
|
||||
|
||||
const emailRaw = body?.email;
|
||||
const password: string | undefined = body?.password;
|
||||
|
||||
if (typeof emailRaw !== "string" || typeof password !== "string") {
|
||||
return NextResponse.json({ message: "email 과 password 는 필수입니다." }, { status: 400 });
|
||||
}
|
||||
|
||||
const email = emailRaw.trim().toLowerCase();
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
if (!user) {
|
||||
return NextResponse.json({ message: "이메일 또는 비밀번호가 올바르지 않습니다." }, { status: 401 });
|
||||
}
|
||||
|
||||
const ok = await verifyPassword(password, user.passwordHash);
|
||||
if (!ok) {
|
||||
return NextResponse.json({ message: "이메일 또는 비밀번호가 올바르지 않습니다." }, { status: 401 });
|
||||
}
|
||||
|
||||
const token = await signAccessToken({ id: user.id, email: user.email, tokenVersion: user.tokenVersion });
|
||||
|
||||
const { passwordHash: _ph, ...safeUser } = user;
|
||||
|
||||
const res = NextResponse.json(safeUser, { status: 200 });
|
||||
|
||||
res.cookies.set("pb_access", token, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ message: "로그인 처리 중 오류가 발생했습니다.", error: error?.message },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// /api/auth/logout
|
||||
// - 클라이언트의 pb_access 세션 쿠키를 제거해 로그아웃을 수행한다.
|
||||
|
||||
export async function POST(_request: Request) {
|
||||
const res = NextResponse.json({ message: "로그아웃 되었습니다." }, { status: 200 });
|
||||
|
||||
// pb_access 쿠키를 즉시 만료시켜 로그아웃 상태로 만든다.
|
||||
res.cookies.set("pb_access", "", {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { verifyAccessToken } from "@/features/auth/authCrypto";
|
||||
|
||||
// /api/auth/me
|
||||
// - 요청 쿠키에 포함된 JWT(pb_access)를 검증하고,
|
||||
// - 유효한 경우 토큰에서 식별 가능한 유저 정보를 반환한다.
|
||||
|
||||
function extractTokenFromCookieHeader(cookieHeader: string | null): string | null {
|
||||
if (!cookieHeader) return null;
|
||||
|
||||
const parts = cookieHeader.split(";");
|
||||
for (const part of parts) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed.startsWith("pb_access=")) {
|
||||
const value = trimmed.substring("pb_access=".length);
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const cookieHeader = request.headers.get("cookie");
|
||||
const token = extractTokenFromCookieHeader(cookieHeader);
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ message: "인증 정보가 없습니다." }, { status: 401 });
|
||||
}
|
||||
|
||||
const payload = await verifyAccessToken(token);
|
||||
|
||||
if (!payload) {
|
||||
return NextResponse.json({ message: "유효하지 않은 토큰입니다." }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = {
|
||||
id: payload.sub,
|
||||
email: payload.email,
|
||||
tokenVersion: payload.tokenVersion,
|
||||
};
|
||||
|
||||
return NextResponse.json(user, { status: 200 });
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
|
||||
import { hashPassword, signAccessToken } from "@/features/auth/authCrypto";
|
||||
|
||||
// /api/auth/signup
|
||||
// - 이메일/비밀번호를 받아 새 유저를 생성하고,
|
||||
// - JWT 액세스 토큰을 httpOnly 쿠키에 설정한 뒤, 비밀번호 해시를 제외한 유저 정보를 반환한다.
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json().catch(() => null as any);
|
||||
|
||||
const emailRaw = body?.email;
|
||||
const password: string | undefined = body?.password;
|
||||
|
||||
if (typeof emailRaw !== "string" || typeof password !== "string") {
|
||||
return NextResponse.json({ message: "email 과 password 는 필수입니다." }, { status: 400 });
|
||||
}
|
||||
|
||||
const email = emailRaw.trim().toLowerCase();
|
||||
|
||||
if (password.length < 8) {
|
||||
return NextResponse.json({ message: "비밀번호는 최소 8자 이상이어야 합니다." }, { status: 400 });
|
||||
}
|
||||
|
||||
// 이미 존재하는 이메일인지 확인한다.
|
||||
const existing = await prisma.user.findUnique({ where: { email } });
|
||||
if (existing) {
|
||||
return NextResponse.json({ message: "이미 가입된 이메일입니다." }, { status: 409 });
|
||||
}
|
||||
|
||||
// 비밀번호를 해시한 뒤 유저를 생성한다.
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
passwordHash,
|
||||
tokenVersion: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const token = await signAccessToken({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
tokenVersion: user.tokenVersion,
|
||||
});
|
||||
|
||||
const { passwordHash: _ph, ...safeUser } = user;
|
||||
|
||||
const res = NextResponse.json(safeUser, { status: 201 });
|
||||
|
||||
res.cookies.set("pb_access", token, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ message: "회원가입 처리 중 오류가 발생했습니다.", error: error?.message },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,60 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
import { verifyAccessToken } from "@/features/auth/authCrypto";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function GET(_request: Request, context: { params: { slug: string } | Promise<{ slug: string }> }) {
|
||||
interface AuthUser {
|
||||
id: string;
|
||||
email: string;
|
||||
tokenVersion: number;
|
||||
}
|
||||
|
||||
function extractTokenFromCookieHeader(cookieHeader: string | null): string | null {
|
||||
if (!cookieHeader) return null;
|
||||
|
||||
const parts = cookieHeader.split(";");
|
||||
for (const part of parts) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed.startsWith("pb_access=")) {
|
||||
const value = trimmed.substring("pb_access=".length);
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getAuthUserFromRequest(request: Request): Promise<AuthUser | null> {
|
||||
const cookieHeader = request.headers.get("cookie");
|
||||
const token = extractTokenFromCookieHeader(cookieHeader);
|
||||
if (!token) return null;
|
||||
|
||||
const payload = await verifyAccessToken(token);
|
||||
if (!payload) return null;
|
||||
|
||||
return {
|
||||
id: payload.sub,
|
||||
email: payload.email,
|
||||
tokenVersion: payload.tokenVersion,
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
context: { params: { slug: string } | Promise<{ slug: string }> },
|
||||
) {
|
||||
const authUser = await getAuthUserFromRequest(request);
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ message: "프로젝트를 조회하려면 로그인이 필요합니다." }, { status: 401 });
|
||||
}
|
||||
|
||||
const { slug } = await context.params;
|
||||
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { slug },
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
if (!project || (project.userId && project.userId !== authUser.id)) {
|
||||
return NextResponse.json({ message: "프로젝트를 찾을 수 없습니다." }, { status: 404 });
|
||||
}
|
||||
|
||||
@@ -18,12 +62,25 @@ export async function GET(_request: Request, context: { params: { slug: string }
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: Request,
|
||||
request: Request,
|
||||
context: { params: { slug: string } | Promise<{ slug: string }> },
|
||||
) {
|
||||
const authUser = await getAuthUserFromRequest(request);
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ message: "프로젝트를 삭제하려면 로그인이 필요합니다." }, { status: 401 });
|
||||
}
|
||||
|
||||
const { slug } = await context.params;
|
||||
|
||||
try {
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { slug },
|
||||
});
|
||||
|
||||
if (!project || (project.userId && project.userId !== authUser.id)) {
|
||||
return NextResponse.json({ message: "프로젝트를 찾을 수 없습니다." }, { status: 404 });
|
||||
}
|
||||
|
||||
await prisma.project.delete({
|
||||
where: { slug },
|
||||
});
|
||||
|
||||
@@ -1,11 +1,53 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
import { verifyAccessToken } from "@/features/auth/authCrypto";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function GET(_request: Request) {
|
||||
interface AuthUser {
|
||||
id: string;
|
||||
email: string;
|
||||
tokenVersion: number;
|
||||
}
|
||||
|
||||
function extractTokenFromCookieHeader(cookieHeader: string | null): string | null {
|
||||
if (!cookieHeader) return null;
|
||||
|
||||
const parts = cookieHeader.split(";");
|
||||
for (const part of parts) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed.startsWith("pb_access=")) {
|
||||
const value = trimmed.substring("pb_access=".length);
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getAuthUserFromRequest(request: Request): Promise<AuthUser | null> {
|
||||
const cookieHeader = request.headers.get("cookie");
|
||||
const token = extractTokenFromCookieHeader(cookieHeader);
|
||||
if (!token) return null;
|
||||
|
||||
const payload = await verifyAccessToken(token);
|
||||
if (!payload) return null;
|
||||
|
||||
return {
|
||||
id: payload.sub,
|
||||
email: payload.email,
|
||||
tokenVersion: payload.tokenVersion,
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authUser = await getAuthUserFromRequest(request);
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ message: "프로젝트 목록을 조회하려면 로그인이 필요합니다." }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const projects = await prisma.project.findMany({
|
||||
where: { userId: authUser.id },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 50,
|
||||
select: {
|
||||
@@ -28,6 +70,11 @@ export async function GET(_request: Request) {
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authUser = await getAuthUserFromRequest(request);
|
||||
if (!authUser) {
|
||||
return NextResponse.json({ message: "프로젝트를 저장하려면 로그인이 필요합니다." }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { title, slug, contentJson } = body;
|
||||
|
||||
@@ -36,6 +83,17 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = await prisma.project.findUnique({
|
||||
where: { slug },
|
||||
});
|
||||
|
||||
if (existing && existing.userId && existing.userId !== authUser.id) {
|
||||
return NextResponse.json(
|
||||
{ message: "이미 다른 사용자가 사용 중인 프로젝트 주소입니다." },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
const project = await prisma.project.upsert({
|
||||
where: { slug },
|
||||
update: {
|
||||
@@ -46,6 +104,7 @@ export async function POST(request: Request) {
|
||||
title,
|
||||
slug,
|
||||
contentJson,
|
||||
userId: authUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user