first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:14:08 +03:00
commit b2825e1698
41 changed files with 14258 additions and 0 deletions

176
lib/auth-api.ts Normal file
View File

@@ -0,0 +1,176 @@
/**
* Auth API client for login, register, me, refresh.
* Uses NEXT_PUBLIC_BASE_API_URL on client (defaults to same as BASE_API_URL for server).
*/
const getBaseUrl = () =>
typeof window !== "undefined"
? process.env.NEXT_PUBLIC_BASE_API_URL ?? "http://127.0.0.1:8080"
: process.env.BASE_API_URL ?? process.env.NEXT_PUBLIC_BASE_API_URL ?? "http://127.0.0.1:8080";
const API_PREFIX = "/api/v1/auth";
export interface AuthUser {
id: number;
email: string;
first_name: string;
last_name: string;
username?: string;
is_admin: boolean;
email_verified?: boolean;
}
export interface LoginResponse {
access_token: string;
refresh_token: string;
user: AuthUser;
}
export interface RegisterResponse {
message: string;
user: AuthUser;
}
export interface RefreshResponse {
access_token: string;
refresh_token: string;
}
/** POST /api/v1/auth/login doğrudan backend (token clientta; cookie için loginViaCookie kullanın) */
export async function login(
email: string,
password: string
): Promise<LoginResponse> {
const base = getBaseUrl();
const res = await fetch(`${base}${API_PREFIX}/login`, {
method: "POST",
headers: { "Content-Type": "application/json", accept: "application/json" },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error((err as { detail?: string }).detail ?? "Giriş başarısız");
}
return res.json();
}
/** Cookie tabanlı giriş tokenlar HTTP-only secure cookiede saklanır */
export async function loginViaCookie(
email: string,
password: string
): Promise<{ user: AuthUser }> {
const res = await fetch("/api/auth/cookie-login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.trim(), password }),
credentials: "include",
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error((data as { error?: string }).error ?? "Giriş başarısız");
}
return data as { user: AuthUser };
}
/** Cookie tabanlı çıkış */
export async function logoutViaCookie(): Promise<void> {
await fetch("/api/auth/cookie-logout", {
method: "POST",
credentials: "include",
});
}
/** Cookie oturumunu kontrol et (client tarafında oturum bilgisi için) */
export async function getCookieSession(): Promise<{
loggedIn: boolean;
user?: AuthUser;
}> {
const res = await fetch("/api/auth/cookie-session", { credentials: "include" });
const data = await res.json().catch(() => ({ loggedIn: false }));
return data as { loggedIn: boolean; user?: AuthUser };
}
/** POST /api/v1/auth/register */
export async function register(body: {
email: string;
first_name: string;
last_name: string;
password: string;
username: string;
}): Promise<RegisterResponse> {
const base = getBaseUrl();
const res = await fetch(`${base}${API_PREFIX}/register`, {
method: "POST",
headers: { "Content-Type": "application/json", accept: "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
const detail = (err as { detail?: string | string[] }).detail;
const message = Array.isArray(detail) ? detail.join(" ") : detail ?? "Kayıt başarısız";
throw new Error(message);
}
return res.json();
}
/** GET /api/v1/auth/me */
export async function me(accessToken: string): Promise<{ user: AuthUser }> {
const base = getBaseUrl();
const res = await fetch(`${base}${API_PREFIX}/me`, {
headers: {
accept: "application/json",
Authorization: `Bearer ${accessToken}`,
},
});
if (!res.ok) throw new Error("Oturum bilgisi alınamadı");
return res.json();
}
/** POST /api/v1/auth/refresh */
export async function refresh(refreshToken: string): Promise<RefreshResponse> {
const base = getBaseUrl();
const res = await fetch(`${base}${API_PREFIX}/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json", accept: "application/json" },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (!res.ok) throw new Error("Token yenilenemedi");
return res.json();
}
const ACCESS_KEY = "auth_access_token";
const REFRESH_KEY = "auth_refresh_token";
const AUTH_CHANGE_EVENT = "auth-change";
/** Header vb. bileşenlerin oturum değişikliğini algılaması için tetiklenir. */
export function notifyAuthChange(): void {
if (typeof window === "undefined") return;
window.dispatchEvent(new Event(AUTH_CHANGE_EVENT));
}
export function setTokens(access: string, refreshToken: string): void {
if (typeof window === "undefined") return;
localStorage.setItem(ACCESS_KEY, access);
localStorage.setItem(REFRESH_KEY, refreshToken);
notifyAuthChange();
}
export function getAccessToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(ACCESS_KEY);
}
export function getRefreshToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(REFRESH_KEY);
}
export function clearTokens(): void {
if (typeof window === "undefined") return;
localStorage.removeItem(ACCESS_KEY);
localStorage.removeItem(REFRESH_KEY);
notifyAuthChange();
}
export { AUTH_CHANGE_EVENT };

25
lib/auth-cookies.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* Cookie adları ve ayarları JWT için HTTP-only secure cookie.
* Sadece sunucu tarafında (API route) kullanılır.
*/
const COOKIE_ACCESS = "auth_access_token";
const COOKIE_REFRESH = "auth_refresh_token";
const isProd = process.env.NODE_ENV === "production";
const COOKIE_OPTS = {
httpOnly: true,
secure: isProd,
sameSite: "lax" as const,
path: "/",
};
const ACCESS_MAX_AGE = 24 * 60 * 60; // 1 gün
const REFRESH_MAX_AGE = 30 * 24 * 60 * 60; // 30 gün
export {
COOKIE_ACCESS,
COOKIE_REFRESH,
COOKIE_OPTS,
ACCESS_MAX_AGE,
REFRESH_MAX_AGE,
};

53
lib/auth-options.ts Normal file
View File

@@ -0,0 +1,53 @@
import type { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import GitHubProvider from "next-auth/providers/github";
/**
* NextAuth options: Google & GitHub OAuth.
* Env: NEXTAUTH_SECRET veya NEXT_AUTH_SECRET, NEXTAUTH_URL,
* GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET,
* GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
*/
const isProd = process.env.NODE_ENV === "production";
export const authOptions: NextAuthOptions = {
secret: process.env.NEXTAUTH_SECRET ?? process.env.NEXT_AUTH_SECRET,
useSecureCookies: isProd,
cookies: {
sessionToken: {
name: `${isProd ? "__Secure-" : ""}next-auth.session-token`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: isProd,
maxAge: 30 * 24 * 60 * 60, // 30 gün
},
},
},
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
}),
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID ?? "",
clientSecret: process.env.GITHUB_CLIENT_SECRET ?? "",
authorization: {
params: {
scope: "user:email",
},
},
}),
],
pages: {
signIn: "/auth/login",
},
callbacks: {
redirect({ url, baseUrl }) {
if (url.startsWith("/")) return `${baseUrl}${url}`;
if (new URL(url).origin === baseUrl) return url;
return baseUrl;
},
},
};

6
lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}