Jiseoup/showmycodePublic
EN|KO
  • 코드
  • 커밋
  • 풀 리퀘스트
proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { locales, defaultLocale, hasLocale } from "@/lib/i18n";
import { COOKIE_NAME, verifyToken, verifyCookie, cookieValue } from "@/lib/auth";

// Paths that are accessible without a valid auth cookie.
const PUBLIC_PATHS = ["/unauthorized"];

function isPublicPath(pathname: string): boolean {
  return PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`));
}

// Redirect to the correct locale if not already present in the path.
function redirectToLocale(request: NextRequest): NextResponse | undefined {
  const { pathname } = request.nextUrl;

  // Pass through if locale prefix already present.
  const hasLocalePrefix = locales.some(
    (l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`),
  );
  if (hasLocalePrefix) return;

  // Detect locale from Accept-Language header.
  const acceptLang = request.headers.get("accept-language") ?? "";
  const detected = acceptLang.split(",")[0].split("-")[0].toLowerCase();
  const locale = hasLocale(detected) ? detected : defaultLocale;

  request.nextUrl.pathname = `/${locale}${pathname}`;
  return NextResponse.redirect(request.nextUrl);
}

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Always allow public paths through without auth.
  if (isPublicPath(pathname)) return NextResponse.next();

  const token = process.env.SHARE_TOKEN;

  // If SHARE_TOKEN is not set, run in public mode (no auth required).
  if (!token) {
    return redirectToLocale(request) ?? NextResponse.next();
  }

  // If ?token= query param is present and valid, set auth cookie and redirect without the param.
  // This allows sharing a plain URL like https://example.com/?token=xxx.
  const queryToken = request.nextUrl.searchParams.get("token");
  if (queryToken !== null) {
    if (verifyToken(queryToken, token)) {
      const url = new URL(request.url);
      url.searchParams.delete("token");
      const response = NextResponse.redirect(url);
      response.cookies.set(COOKIE_NAME, cookieValue(token), {
        httpOnly: true,
        sameSite: "lax",
        secure: process.env.NODE_ENV === "production",
        path: "/",
        maxAge: 60 * 60 * 24 * 30, // 30 days
      });
      return response;
    }
    // Invalid token in query param — deny access.
    return NextResponse.redirect(new URL("/unauthorized", request.url));
  }

  // Check auth cookie for subsequent requests.
  const cookie = request.cookies.get(COOKIE_NAME);
  if (cookie?.value && verifyCookie(cookie.value, token)) {
    return redirectToLocale(request) ?? NextResponse.next();
  }

  // No valid auth — redirect to unauthorized page.
  return NextResponse.redirect(new URL("/unauthorized", request.url));
}

/**
 * Run the proxy on every page route, excluding only framework internals and named static assets.
 *
 * Dotted paths are intentionally not excluded:
 * repository names can contain dots (e.g. `next.js`),
 * so a blanket `.*\..*` rule would let those repo pages bypass the share-token check.
 *
 * When adding files to `public/` (e.g. robots.txt, og images),
 * list them here — otherwise they hit the auth gate and locale redirect.
 */
export const config = {
  matcher: ["/((?!_next/static|_next/image|api|favicon.ico|icon.svg).*)"],
};
showmycode
  • .editorconfig
  • .env.example
  • .gitattributes
  • .gitignore
  • .prettierignore
  • .prettierrc.json
  • AGENTS.md
  • CLAUDE.md
  • CODE_OF_CONDUCT.md
  • components.json
  • CONTRIBUTING.md
  • eslint.config.mjs
  • LICENSE
  • next.config.ts
  • package-lock.json
  • package.json
  • postcss.config.mjs
  • proxy.ts
  • README.ko.md
  • README.md
  • SECURITY.md
  • tsconfig.json