commit b2825e16984f1b6309006a046a0049fcbf8a5522 Author: Beyhan Oğur Date: Sun Apr 26 22:14:08 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..62a9268 --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,6 @@ +import NextAuth from "next-auth"; +import { authOptions } from "@/lib/auth-options"; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/app/api/auth/cookie-login/route.ts b/app/api/auth/cookie-login/route.ts new file mode 100644 index 0000000..5ed72fc --- /dev/null +++ b/app/api/auth/cookie-login/route.ts @@ -0,0 +1,96 @@ +import { NextRequest, NextResponse } from "next/server"; +import { cookies } from "next/headers"; +import { + COOKIE_ACCESS, + COOKIE_REFRESH, + COOKIE_OPTS, + ACCESS_MAX_AGE, + REFRESH_MAX_AGE, +} from "@/lib/auth-cookies"; + +const BASE_URL = + process.env.BASE_API_URL ?? + process.env.NEXT_PUBLIC_BASE_API_URL ?? + "http://127.0.0.1:8080"; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { email, password } = body as { email?: string; password?: string }; + if (!email || !password) { + return NextResponse.json( + { error: "E-posta ve şifre gerekli." }, + { status: 400 } + ); + } + + let res: Response; + try { + res = await fetch(`${BASE_URL}/api/v1/auth/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + accept: "application/json", + }, + body: JSON.stringify({ email: String(email).trim(), password }), + }); + } catch (fetchErr) { + const msg = + process.env.NODE_ENV === "development" && fetchErr instanceof Error + ? `Backend erişilemedi: ${fetchErr.message} (URL: ${BASE_URL})` + : "Giriş servisi şu an kullanılamıyor."; + return NextResponse.json({ error: msg }, { status: 502 }); + } + + let data: unknown; + try { + const text = await res.text(); + data = text ? JSON.parse(text) : {}; + } catch { + data = {}; + } + + if (!res.ok) { + const message = + (data as { detail?: string })?.detail ?? "Giriş başarısız"; + return NextResponse.json( + { error: message }, + { status: res.status >= 400 ? res.status : 500 } + ); + } + + const access_token = (data as { access_token?: string })?.access_token; + const refresh_token = (data as { refresh_token?: string })?.refresh_token; + const user = (data as { user?: unknown })?.user; + + if (!access_token || !refresh_token) { + return NextResponse.json( + { + error: + process.env.NODE_ENV === "development" + ? "Backend token döndürmedi." + : "Giriş yanıtı geçersiz.", + }, + { status: 502 } + ); + } + + const cookieStore = await cookies(); + cookieStore.set(COOKIE_ACCESS, access_token, { + ...COOKIE_OPTS, + maxAge: ACCESS_MAX_AGE, + }); + cookieStore.set(COOKIE_REFRESH, refresh_token, { + ...COOKIE_OPTS, + maxAge: REFRESH_MAX_AGE, + }); + + return NextResponse.json({ user }); + } catch (e) { + const message = + process.env.NODE_ENV === "development" && e instanceof Error + ? e.message + : "Sunucu hatası."; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/app/api/auth/cookie-logout/route.ts b/app/api/auth/cookie-logout/route.ts new file mode 100644 index 0000000..b60b424 --- /dev/null +++ b/app/api/auth/cookie-logout/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; +import { COOKIE_ACCESS, COOKIE_REFRESH, COOKIE_OPTS } from "@/lib/auth-cookies"; + +export async function POST() { + const cookieStore = await cookies(); + cookieStore.set(COOKIE_ACCESS, "", { ...COOKIE_OPTS, maxAge: 0 }); + cookieStore.set(COOKIE_REFRESH, "", { ...COOKIE_OPTS, maxAge: 0 }); + return NextResponse.json({ ok: true }); +} diff --git a/app/api/auth/cookie-session/route.ts b/app/api/auth/cookie-session/route.ts new file mode 100644 index 0000000..c064b2b --- /dev/null +++ b/app/api/auth/cookie-session/route.ts @@ -0,0 +1,34 @@ +import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; +import { COOKIE_ACCESS } from "@/lib/auth-cookies"; + +const BASE_URL = + process.env.BASE_API_URL ?? process.env.NEXT_PUBLIC_BASE_API_URL ?? "http://127.0.0.1:8080"; + +export async function GET() { + const cookieStore = await cookies(); + const accessToken = cookieStore.get(COOKIE_ACCESS)?.value; + + if (!accessToken) { + return NextResponse.json({ loggedIn: false }); + } + + try { + const res = await fetch(`${BASE_URL}/api/v1/auth/me`, { + headers: { + accept: "application/json", + Authorization: `Bearer ${accessToken}`, + }, + }); + if (!res.ok) { + return NextResponse.json({ loggedIn: false }); + } + const data = await res.json(); + return NextResponse.json({ + loggedIn: true, + user: (data as { user?: unknown }).user, + }); + } catch { + return NextResponse.json({ loggedIn: false }); + } +} diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx new file mode 100644 index 0000000..9f52f10 --- /dev/null +++ b/app/auth/login/page.tsx @@ -0,0 +1,153 @@ +"use client"; + +import React, { useRef, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { Turnstile, type TurnstileRef } from "nextjs-turnstile"; +import { Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { loginViaCookie } from "@/lib/auth-api"; +import { AuthSocialButtons } from "@/components/auth-social-buttons"; +import { cn } from "@/lib/utils"; + +const TURNSTILE_SITE_KEY = + process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ?? process.env.NEXT_PUBLIC_CLOUD_FLARE_SITE_KEY ?? ""; + +export default function LoginPage() { + const router = useRouter(); + const turnstileRef = useRef(null); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [turnstileToken, setTurnstileToken] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + if (!email.trim()) { + setError("E-posta gerekli."); + return; + } + if (!password) { + setError("Şifre gerekli."); + return; + } + if (TURNSTILE_SITE_KEY && !turnstileToken) { + setError("Lütfen doğrulamayı tamamlayın."); + return; + } + setLoading(true); + try { + await loginViaCookie(email.trim(), password); + if (typeof window !== "undefined") window.dispatchEvent(new Event("auth-change")); + router.push("/"); + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : "Giriş yapılamadı."); + turnstileRef.current?.reset(); + setTurnstileToken(null); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+

+ Giriş yap +

+

+ Hesabınıza giriş yapın +

+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + disabled={loading} + className={cn( + "w-full rounded-lg border-neutral-200 dark:border-neutral-700" + )} + /> +
+ +
+ + setPassword(e.target.value)} + disabled={loading} + className="w-full rounded-lg border-neutral-200 dark:border-neutral-700" + /> +
+ + {TURNSTILE_SITE_KEY && ( +
+ setTurnstileToken(null)} + onError={() => setTurnstileToken(null)} + /> +
+ )} + + + + + + +

+ Hesabınız yok mu?{" "} + + Kayıt olun + +

+
+
+
+ ); +} diff --git a/app/auth/register/page.tsx b/app/auth/register/page.tsx new file mode 100644 index 0000000..a1f9374 --- /dev/null +++ b/app/auth/register/page.tsx @@ -0,0 +1,227 @@ +"use client"; + +import React, { useRef, useState } from "react"; +import Link from "next/link"; +import { Turnstile, type TurnstileRef } from "nextjs-turnstile"; +import { Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { register } from "@/lib/auth-api"; +import { AuthSocialButtons } from "@/components/auth-social-buttons"; +import { cn } from "@/lib/utils"; + +const TURNSTILE_SITE_KEY = + process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ?? process.env.NEXT_PUBLIC_CLOUD_FLARE_SITE_KEY ?? ""; + +export default function RegisterPage() { + const turnstileRef = useRef(null); + const [email, setEmail] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [turnstileToken, setTurnstileToken] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSuccess(null); + if (!email.trim()) { + setError("E-posta gerekli."); + return; + } + if (!firstName.trim()) { + setError("Ad gerekli."); + return; + } + if (!lastName.trim()) { + setError("Soyad gerekli."); + return; + } + if (!username.trim()) { + setError("Kullanıcı adı gerekli."); + return; + } + if (password.length < 6) { + setError("Şifre en az 6 karakter olmalıdır."); + return; + } + if (TURNSTILE_SITE_KEY && !turnstileToken) { + setError("Lütfen doğrulamayı tamamlayın."); + return; + } + setLoading(true); + try { + const res = await register({ + email: email.trim(), + first_name: firstName.trim(), + last_name: lastName.trim(), + username: username.trim(), + password, + }); + setSuccess( + res.message ?? "Kayıt başarılı. Giriş yapmadan önce e-posta doğrulaması yapın." + ); + turnstileRef.current?.reset(); + setTurnstileToken(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Kayıt oluşturulamadı."); + turnstileRef.current?.reset(); + setTurnstileToken(null); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+

+ Kayıt ol +

+

+ Yeni hesap oluşturun +

+ +
+ {error && ( +
+ {error} +
+ )} + {success && ( +
+ {success} +
+ )} + +
+
+ + setFirstName(e.target.value)} + disabled={loading} + className="rounded-lg border-neutral-200 dark:border-neutral-700" + /> +
+
+ + setLastName(e.target.value)} + disabled={loading} + className="rounded-lg border-neutral-200 dark:border-neutral-700" + /> +
+
+ +
+ + setUsername(e.target.value)} + disabled={loading} + className={cn( + "w-full rounded-lg border-neutral-200 dark:border-neutral-700" + )} + /> +
+ +
+ + setEmail(e.target.value)} + disabled={loading} + className="w-full rounded-lg border-neutral-200 dark:border-neutral-700" + /> +
+ +
+ + setPassword(e.target.value)} + disabled={loading} + className="w-full rounded-lg border-neutral-200 dark:border-neutral-700" + /> +
+ + {TURNSTILE_SITE_KEY && ( +
+ setTurnstileToken(null)} + onError={() => setTurnstileToken(null)} + /> +
+ )} + + + + + + +

+ Zaten hesabınız var mı?{" "} + + Giriş yapın + +

+
+
+
+ ); +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..382ca14 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,126 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..817d737 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,49 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; +import Header from "@/components/header"; +import { ThemeProvider } from "@/components/theme-provider"; +import { SessionProvider } from "@/components/providers/session-provider"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +