diff --git a/backend/src/main.cc b/backend/src/main.cc index fee8f46..061671c 100644 --- a/backend/src/main.cc +++ b/backend/src/main.cc @@ -11,6 +11,28 @@ int main(int argc, char** argv) { csp::AppState::Instance().Init(db_path); + // CORS (dev-friendly). In production, prefer reverse proxy same-origin. + drogon::app().registerPreRoutingAdvice([](const drogon::HttpRequestPtr& req, + drogon::AdviceCallback&& cb, + drogon::AdviceChainCallback&& chainCb) { + if (req->method() == drogon::Options) { + auto resp = drogon::HttpResponse::newHttpResponse(); + resp->setStatusCode(drogon::k200OK); + resp->addHeader("Access-Control-Allow-Origin", "*"); + resp->addHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS"); + resp->addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + cb(resp); + return; + } + chainCb(); + }); + + drogon::app().registerPostHandlingAdvice([](const drogon::HttpRequestPtr&, + const drogon::HttpResponsePtr& resp) { + resp->addHeader("Access-Control-Allow-Origin", "*"); + resp->addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + }); + drogon::app() .addListener("0.0.0.0", 8080) .setThreadNum(4); diff --git a/docker-compose.yml b/docker-compose.yml index c285c16..b552163 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,8 +14,10 @@ services: context: . dockerfile: Dockerfile.frontend environment: - # 前端调用后端API时使用;后续页面会读取该变量 + # 浏览器侧请求后端的公共地址(本机测试用 localhost) - NEXT_PUBLIC_API_BASE=http://localhost:8080 + # Next.js 服务端反代用(可选),仅在你把 NEXT_PUBLIC_API_BASE 设为 /api 时需要 + - BACKEND_INTERNAL_URL=http://backend:8080 ports: - "3000:3000" depends_on: diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..66ee460 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,21 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + // For local dev convenience. In production you should configure reverse proxy + // and set NEXT_PUBLIC_API_BASE to your public API origin. + async rewrites() { + // If the user sets NEXT_PUBLIC_API_BASE to "/api", we can proxy to backend + // from the Next.js server (SSR). This does NOT affect browser fetch. + const backendInternal = process.env.BACKEND_INTERNAL_URL; + if (!backendInternal) return []; + + return [ + { + source: "/api/:path*", + destination: `${backendInternal}/api/:path*`, + }, + ]; + }, }; export default nextConfig; diff --git a/frontend/src/app/auth/page.tsx b/frontend/src/app/auth/page.tsx new file mode 100644 index 0000000..c120850 --- /dev/null +++ b/frontend/src/app/auth/page.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { useMemo, useState } from "react"; + +type AuthOk = { ok: true; user_id: number; token: string; expires_at: number }; +type AuthErr = { ok: false; error: string }; +type AuthResp = AuthOk | AuthErr; + +export default function AuthPage() { + const apiBase = useMemo( + () => process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8080", + [] + ); + + const [mode, setMode] = useState<"register" | "login">("register"); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [resp, setResp] = useState(null); + + async function submit() { + setLoading(true); + setResp(null); + try { + const r = await fetch(`${apiBase}/api/v1/auth/${mode}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + const j = (await r.json()) as AuthResp; + setResp(j); + } catch (e: unknown) { + setResp({ ok: false, error: String(e) }); + } finally { + setLoading(false); + } + } + + return ( +
+
+

{mode === "register" ? "注册" : "登录"}

+

+ API Base: {apiBase} +

+ +
+ + +
+ +
+ + setUsername(e.target.value)} + placeholder="alice" + /> + + + setPassword(e.target.value)} + placeholder="password123" + /> + + +
+ +
+

响应

+
+            {JSON.stringify(resp, null, 2)}
+          
+ {resp && resp.ok && ( +

+ 你可以把 token 用在后续需要登录的接口里: + Authorization: Bearer {resp.token} +

+ )} +
+
+
+ ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 295f8fd..23d4550 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,63 +1,46 @@ -import Image from "next/image"; +import Link from "next/link"; + +const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8080"; + +async function fetchHealth() { + try { + const r = await fetch(`${API_BASE}/api/health`, { cache: "no-store" }); + if (!r.ok) return { ok: false, error: `HTTP ${r.status}` } as const; + return (await r.json()) as { ok: boolean; version?: string }; + } catch (e: unknown) { + return { ok: false, error: String(e) } as const; + } +} + +export default async function Home() { + const health = await fetchHealth(); -export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+
+
+

CSP 在线练习平台(MVP)

+

+ API Base: {API_BASE} +

+ +
+

后端状态

+
+            {JSON.stringify(health, null, 2)}
+          
- + +
+ 说明:当前前端仅用于验证注册/登录与基础连通性;题库/提交/比赛/判题会在后续迭代补齐。