{"id":569,"date":"2026-06-15T03:06:17","date_gmt":"2026-06-15T03:06:17","guid":{"rendered":"https:\/\/hyc.eshachem.com\/program\/?page_id=569"},"modified":"2026-06-15T10:21:12","modified_gmt":"2026-06-15T10:21:12","slug":"sso-%e5%96%ae%e4%b8%80%e7%99%bb%e5%85%a5%e6%8a%80%e8%a1%93%e6%96%87%e4%bb%b6-next-js","status":"publish","type":"page","link":"https:\/\/hyc.eshachem.com\/program\/birc\/sso-%e5%96%ae%e4%b8%80%e7%99%bb%e5%85%a5%e6%8a%80%e8%a1%93%e6%96%87%e4%bb%b6-next-js\/","title":{"rendered":"Next.js &#8211; SSO in BIRC"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">\u672c\u6587\u4ef6\u900f\u904e\u5546\u696d\u667a\u6167\u7814\u7a76\u4e2d\u5fc3 \u2013 \u667a\u6167\u6821\u5712:\u6d3b\u52d5\u7cfb\u7d71\u4f86\u8aaa\u660e<strong>\u5546\u667a\u4e2d\u5fc3\u7684SSO\u67b6\u69cb<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5546\u667a\u4e2d\u5fc3\u7684SSO\u70ba\u81ea\u67b6\u4f3a\u670d\u5668\uff0c<strong>\u975e<\/strong>Google SSO\uff0c\u4f46\u539f\u7406\u5927\u540c\u5c0f\u7570\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8a72\u6587\u7ae0\u6240\u8ff0\u7a0b\u5f0f\u78bc\u7686\u63d0\u4f9b\u65bc\u6587\u7ae0\u6700\u4e0b\u65b9GitHub\u4e2d\u3002<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u7248\u672c\uff1a0.1.0<br>\u6700\u5f8c\u66f4\u65b0\u6642\u9593\uff1a06\/10\/2026<br>\u6700\u5f8c\u66f4\u65b0\u4eba\u54e1\uff1a\u9673\u6cd3\u6bd3<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e8%a8%ad%e8%a8%88%e5%8e%9f%e5%89%87\">\u8a2d\u8a08\u539f\u5247<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u539f\u5247<\/th><th>\u8aaa\u660e<\/th><\/tr><\/thead><tbody><tr><td><strong>\u524d\u7aef\u4e0d\u6301\u6709 Client Secret<\/strong><\/td><td>\u524d\u7aef<strong>\u7d55\u4e0d<\/strong>\u76f4\u63a5\u547c\u53eb SSO <code>\/sso\/verify-code<\/code><\/td><\/tr><tr><td><strong>\u6388\u6b0a\u78bc\u50c5\u7528\u65bc\u4ea4\u63db<\/strong><\/td><td>SSO \u56de\u50b3\u7684 <code>code<\/code> \u70ba\u4e00\u6b21\u6027\u6388\u6b0a\u78bc\uff0c\u61c9\u7acb\u5373\u8f49\u4ea4\u5f8c\u7aef\u4ea4\u63db JWT<\/td><\/tr><tr><td><strong>JWT \u4f5c\u70ba\u5f8c\u7e8c\u8a8d\u8b49<\/strong><\/td><td>\u767b\u5165\u5b8c\u6210\u5f8c\uff0c\u6240\u6709\u53d7\u4fdd\u8b77 API \u4ee5 <code>Authorization: Bearer {JWT}<\/code> \u5b58\u53d6<\/td><\/tr><tr><td><strong>\u5168\u7ad9\u56de\u8abf\u8655\u7406<\/strong><\/td><td>SSO \u53ef\u56de\u8df3\u81f3\u4efb\u610f\u9801\u9762\uff0c<code>AuthProvider<\/code> \u7d71\u4e00\u6514\u622a <code>?code=<\/code> \u53c3\u6578<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e7%b3%bb%e7%b5%b1%e8%a7%92%e8%89%b2\">\u7cfb\u7d71\u89d2\u8272<\/h2>\n\n\n\n<div class=\"wp-block-merpress-mermaidjs diagram-source-mermaid\"><pre class=\"mermaid\">flowchart LR\n    User[\u4f7f\u7528\u8005]\n    FE[Campus Activity \u524d\u7aef]\n    SSO[SSO \u767b\u5165\u4e2d\u5fc3]\n    BE[Campus Activity \u5f8c\u7aef]\n\n    User --&gt;|\u9ede\u64ca\u767b\u5165| FE\n    FE --&gt;|GET \/sso\/prelogin| SSO\n    SSO --&gt;|302 ?code=| FE\n    FE --&gt;|POST \/auth-tokens\/exchange| BE\n    BE --&gt;|X-Auth-Token JWT| FE\n    FE --&gt;|Bearer JWT| BE<\/pre><\/div>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u89d2\u8272<\/th><th>\u8077\u8cac<\/th><\/tr><\/thead><tbody><tr><td><strong>SSO \u767b\u5165\u4e2d\u5fc3<\/strong><\/td><td>Google \u7b49 IdP \u767b\u5165\u3001\u6838\u767c\u4e00\u6b21\u6027\u6388\u6b0a\u78bc<\/td><\/tr><tr><td><strong>\u524d\u7aef<\/strong><\/td><td>\u5c0e\u5411 SSO\u3001\u63a5\u6536 <code>code<\/code>\u3001\u5411\u5f8c\u7aef\u4ea4\u63db JWT\u3001\u5132\u5b58\u4e26\u9644\u5e36 JWT \u547c\u53eb API<\/td><\/tr><tr><td><strong>\u5f8c\u7aef<\/strong><\/td><td>\u4ee5 Client Secret \u9a57\u8b49 <code>code<\/code>\u3001\u5efa\u7acb\/\u66f4\u65b0\u4f7f\u7528\u8005\u3001\u6838\u767c JWT<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e6%95%b4%e9%ab%94%e6%b5%81%e7%a8%8b\">\u6574\u9ad4\u6d41\u7a0b<\/h2>\n\n\n\n<div class=\"wp-block-merpress-mermaidjs diagram-source-mermaid\"><pre class=\"mermaid\">sequenceDiagram\n    participant User as \u4f7f\u7528\u8005\n    participant Navbar as Navbar \/ AuthContext\n    participant SSO as SSO \u767b\u5165\u4e2d\u5fc3\n    participant BE as Campus Activity \u5f8c\u7aef\n\n    User-&gt;&gt;Navbar: \u9ede\u64ca\u53f3\u4e0a\u89d2\u300c\u767b\u5165\u300d\n    Navbar-&gt;&gt;SSO: GET \/sso\/prelogin?redirect_url={\u76ee\u524d\u9801\u9762\u7db2\u5740}\n    SSO-&gt;&gt;User: \u986f\u793a Google \u767b\u5165\u9801\n    User-&gt;&gt;SSO: \u5b8c\u6210\u8eab\u5206\u9a57\u8b49\n    SSO-&gt;&gt;Navbar: 302 \u8df3\u8f49 redirect_url?code={\u6388\u6b0a\u78bc}\n    Navbar-&gt;&gt;BE: POST \/auth-tokens\/exchange&lt;br\/&gt;Header: X-Client-Token: {code}\n    BE-&gt;&gt;SSO: POST \/sso\/verify-code\uff08\u50c5\u5f8c\u7aef\uff09\n    SSO--&gt;&gt;BE: { email, name, picture }\n    BE--&gt;&gt;Navbar: 200 OK&lt;br\/&gt;Header: X-Auth-Token: {JWT}\n    Navbar-&gt;&gt;Navbar: \u89e3\u6790 JWT\u3001\u5132\u5b58 authToken\u3001\u66f4\u65b0 UI\n    User-&gt;&gt;Navbar: \u53f3\u4e0a\u89d2\u986f\u793a\u982d\u50cf\uff08Google \u7167\u7247\u6216\u540d\u7a31\u9996\u5b57\uff09\n    User-&gt;&gt;Navbar: \u9ede\u64ca\u982d\u50cf\u9032\u5165 \/profile<\/pre><\/div>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e7%92%b0%e5%a2%83%e8%ae%8a%e6%95%b8\">\u74b0\u5883\u8b8a\u6578<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u8b8a\u6578<\/th><th>\u5fc5\u586b<\/th><th>\u8aaa\u660e<\/th><th>\u7bc4\u4f8b<\/th><\/tr><\/thead><tbody><tr><td><code>NEXT_PUBLIC_API_URL<\/code><\/td><td>\u662f<\/td><td>\u5f8c\u7aef API Base URL\uff08\u542b <code>\/api<\/code> \u524d\u7db4\uff09<\/td><td><code>https:\/\/api.example.com\/api<\/code><\/td><\/tr><tr><td><code>NEXT_PUBLIC_SSO_BASE_URL<\/code><\/td><td>\u5426<\/td><td>SSO \u767b\u5165\u4e2d\u5fc3 Base URL<\/td><td><code>https:\/\/sso.ntubimdbirc.tw<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>.env.local<\/code> \u7bc4\u4f8b\uff1a<\/strong><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">NEXT_PUBLIC_API_URL=http:<span class=\"hljs-comment\">\/\/localhost:8080\/api<\/span>\nNEXT_PUBLIC_SSO_BASE_URL=https:<span class=\"hljs-comment\">\/\/sso.ntubimdbirc.tw<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e5%89%8d%e7%ab%af%e5%af%a6%e4%bd%9c\">\u524d\u7aef\u5be6\u4f5c<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e6%aa%94%e6%a1%88%e8%81%b7%e8%b2%ac\">\u6a94\u6848\u8077\u8cac<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u6a94\u6848<\/th><th>\u8077\u8cac<\/th><\/tr><\/thead><tbody><tr><td><code>lib\/sso.ts<\/code><\/td><td>SSO \u5c0e\u5411 URL\u3001\u5411\u5f8c\u7aef\u4ea4\u63db JWT\u3001JWT \u89e3\u6790\u3001Token \u5132\u5b58<\/td><\/tr><tr><td><code>lib\/api.ts<\/code><\/td><td>\u9644\u5e36 <code>Authorization: Bearer<\/code> \u7684 API \u8acb\u6c42\u5c01\u88dd<\/td><\/tr><tr><td><code>lib\/hooks\/AuthContext.tsx<\/code><\/td><td>\u5168\u7ad9\u8a8d\u8b49\u72c0\u614b\u3001SSO \u56de\u8abf\u8655\u7406\u3001\u540c\u6b65 EventsContext \u4f7f\u7528\u8005<\/td><\/tr><tr><td><code>components\/layout\/Navbar.tsx<\/code><\/td><td>\u672a\u767b\u5165\u986f\u793a\u300c\u767b\u5165\u300d\u3001\u5df2\u767b\u5165\u986f\u793a\u982d\u50cf\u4e26\u9023\u7d50 <code>\/profile<\/code><\/td><\/tr><tr><td><code>app\/layout.tsx<\/code><\/td><td>\u639b\u8f09 <code>AuthProvider<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e7%99%bc%e8%b5%b7%e7%99%bb%e5%85%a5\">\u767c\u8d77\u767b\u5165<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4f7f\u7528\u8005\u9ede\u64ca Navbar\u300c\u767b\u5165\u300d\u6642\uff0c\u547c\u53eb <code>startSsoLogin()<\/code>\uff1a<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> redirectUrl = <span class=\"hljs-built_in\">window<\/span>.location.origin + pathname;\n<span class=\"hljs-built_in\">window<\/span>.location.href = buildSsoLoginUrl(redirectUrl);\n<span class=\"hljs-comment\">\/\/ \u2192 https:\/\/sso.ntubimdbirc.tw\/sso\/prelogin?redirect_url={encodeURIComponent(redirectUrl)}<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">\u5be6\u4f5c\u4f4d\u7f6e\uff1a<code>lib\/sso.ts<\/code> \u2192 <code>buildSsoLoginUrl()<\/code><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e8%99%95%e7%90%86-sso-%e5%9b%9e%e8%aa%bf\">\u8655\u7406 SSO \u56de\u8abf<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>AuthProvider<\/code> \u76e3\u807d URL \u67e5\u8a62\u53c3\u6578\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u53c3\u6578<\/th><th>\u8655\u7406\u65b9\u5f0f<\/th><\/tr><\/thead><tbody><tr><td><code>?error=...<\/code><\/td><td>\u986f\u793a\u932f\u8aa4 Toast\uff0c\u6e05\u9664 URL \u53c3\u6578<\/td><\/tr><tr><td><code>?code=...<\/code><\/td><td>\u547c\u53eb\u5f8c\u7aef\u4ea4\u63db JWT\uff0c\u6210\u529f\u5f8c <code>router.replace(pathname)<\/code> \u6e05\u9664 code<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ lib\/hooks\/AuthContext.tsx\uff08\u6458\u8981\uff09<\/span>\n<span class=\"hljs-keyword\">const<\/span> code = searchParams.get(<span class=\"hljs-string\">'code'<\/span>);\n<span class=\"hljs-keyword\">if<\/span> (code) {\n  <span class=\"hljs-keyword\">const<\/span> { token, user } = <span class=\"hljs-keyword\">await<\/span> exchangeCodeForToken(code);\n  saveAuthToken(token);\n  router.replace(pathname); <span class=\"hljs-comment\">\/\/ \u6e05\u9664 URL \u4e2d\u7684 code<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"%e5%90%91%e5%be%8c%e7%ab%af%e4%ba%a4%e6%8f%9b-jwt\">\u5411\u5f8c\u7aef\u4ea4\u63db JWT<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ lib\/sso.ts<\/span>\n<span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> fetch(<span class=\"hljs-string\">`<span class=\"hljs-subst\">${NEXT_PUBLIC_API_URL}<\/span>\/auth-tokens\/exchange`<\/span>, {\n  <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">'POST'<\/span>,\n  <span class=\"hljs-attr\">headers<\/span>: {\n    <span class=\"hljs-string\">'X-Client-Token'<\/span>: code,\n  },\n});\n\n<span class=\"hljs-keyword\">const<\/span> jwt = response.headers.get(<span class=\"hljs-string\">'X-Auth-Token'<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u9805\u76ee<\/th><th>\u8aaa\u660e<\/th><\/tr><\/thead><tbody><tr><td>\u7aef\u9ede<\/td><td><code>POST {NEXT_PUBLIC_API_URL}\/auth-tokens\/exchange<\/code><\/td><\/tr><tr><td>\u8acb\u6c42 Header<\/td><td><code>X-Client-Token: {SSO \u6388\u6b0a\u78bc}<\/code><\/td><\/tr><tr><td>\u6210\u529f\u56de\u61c9 Header<\/td><td><code>X-Auth-Token: {JWT}<\/code><\/td><\/tr><tr><td>\u8a8d\u8b49\u9700\u6c42<\/td><td>\u4e0d\u9700\u8981 Bearer Token\uff08\u516c\u958b\u7aef\u9ede\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>CORS\uff1a<\/strong> \u5f8c\u7aef\u5df2\u5c07 <code>X-Auth-Token<\/code> \u52a0\u5165 <code>exposedHeaders<\/code>\uff0c\u524d\u7aef\u624d\u80fd\u5f9e Response Header \u8b80\u53d6 JWT\u3002<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"jwt-%e5%84%b2%e5%ad%98%e8%88%87%e8%a7%a3%e6%9e%90\">JWT \u5132\u5b58\u8207\u89e3\u6790<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u767b\u5165\u6210\u529f\u5f8c\uff0cJWT \u5132\u5b58\u65bc <code>localStorage<\/code>\uff1a<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">localStorage.setItem(<span class=\"hljs-string\">'authToken'<\/span>, jwt);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">\u4f7f\u7528\u8005\u8cc7\u8a0a\u5f9e JWT Payload \u89e3\u6790\uff08\u4e0d\u9700\u53e6\u5916\u5132\u5b58\u4f7f\u7528\u8005\u7269\u4ef6\uff09\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>JWT Claim<\/th><th>\u524d\u7aef\u7528\u9014<\/th><\/tr><\/thead><tbody><tr><td><code>sub<\/code><\/td><td>\u5b78\u6821\u4fe1\u7bb1\uff08<code>email<\/code>\uff09<\/td><\/tr><tr><td><code>id<\/code><\/td><td>\u672c\u5730\u4f7f\u7528\u8005 ID<\/td><\/tr><tr><td><code>name<\/code><\/td><td>\u986f\u793a\u540d\u7a31<\/td><\/tr><tr><td><code>picture<\/code><\/td><td>Google \u982d\u50cf URL<\/td><\/tr><tr><td><code>role<\/code><\/td><td>\u89d2\u8272\uff08\u5982 <code>ROLE_TEACHER_STUDENT<\/code>\uff09<\/td><\/tr><tr><td><code>exp<\/code><\/td><td>\u904e\u671f\u6642\u9593\uff0c\u7528\u65bc\u5224\u65b7\u662f\u5426\u9700\u91cd\u65b0\u767b\u5165<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u5be6\u4f5c\u4f4d\u7f6e\uff1a<code>lib\/sso.ts<\/code> \u2192 <code>parseJwtPayload()<\/code>\u3001<code>authUserFromJwt()<\/code>\u3001<code>isTokenExpired()<\/code><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e5%91%bc%e5%8f%ab%e5%8f%97%e4%bf%9d%e8%ad%b7-api\">\u547c\u53eb\u53d7\u4fdd\u8b77 API<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4f7f\u7528 <code>lib\/api.ts<\/code> \u5c01\u88dd\uff1a<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> { apiJson, apiFetch } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@\/lib\/api'<\/span>;\n\n<span class=\"hljs-comment\">\/\/ GET \u7bc4\u4f8b<\/span>\n<span class=\"hljs-keyword\">const<\/span> events = <span class=\"hljs-keyword\">await<\/span> apiJson&lt;Event[]&gt;(<span class=\"hljs-string\">'\/events'<\/span>);\n\n<span class=\"hljs-comment\">\/\/ \u81ea\u8a02\u8acb\u6c42<\/span>\n<span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> apiFetch(<span class=\"hljs-string\">'\/events'<\/span>, { <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">'GET'<\/span> });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\"><code>apiFetch<\/code> \u6703\u81ea\u52d5\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u5f9e <code>localStorage<\/code> \u8b80\u53d6 <code>authToken<\/code><\/li>\n\n\n\n<li>\u9644\u52a0 <code>Authorization: Bearer {JWT}<\/code><\/li>\n\n\n\n<li>\u6536\u5230 <code>401<\/code> \u6642\u6e05\u9664 Token \u4e26\u62cb\u51fa <code>ApiError<\/code><\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ui-%e8%a1%8c%e7%82%ba\">UI \u884c\u70ba<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Navbar \u53f3\u4e0a\u89d2<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u72c0\u614b<\/th><th>\u986f\u793a<\/th><th>\u884c\u70ba<\/th><\/tr><\/thead><tbody><tr><td>\u672a\u767b\u5165<\/td><td>\u300c\u767b\u5165\u300d\u6309\u9215<\/td><td>\u89f8\u767c <code>startSsoLogin()<\/code><\/td><\/tr><tr><td>\u5df2\u767b\u5165<\/td><td>\u5713\u5f62\u982d\u50cf<\/td><td>\u6709 <code>picture<\/code> \u986f\u793a Google \u7167\u7247\uff0c\u5426\u5247\u986f\u793a\u540d\u7a31\u9996\u5b57<\/td><\/tr><tr><td>\u5df2\u767b\u5165\u9ede\u64ca\u982d\u50cf<\/td><td>\u2014<\/td><td>\u5c0e\u5411 <code>\/profile<\/code> \u500b\u4eba\u9801<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u624b\u6a5f\u7248 Overlay \u9078\u55ae\u4ea6\u6709\u5c0d\u61c9\u7684\u767b\u5165\u6309\u9215\u8207\u982d\u50cf\u9023\u7d50\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u500b\u4eba\u9801\u540c\u6b65<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u767b\u5165\u6210\u529f\u5f8c\uff0c<code>AuthContext<\/code> \u6703\u5c07 JWT \u4e2d\u7684\u4f7f\u7528\u8005\u8cc7\u6599\u540c\u6b65\u81f3 <code>EventsContext<\/code>\uff0c\u4f9b <code>\/profile<\/code> \u7b49\u9801\u9762\u4f7f\u7528\uff1a<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ role \u5c0d\u61c9<\/span>\nROLE_ADMIN \/ ROLE_ACTIVITY_ADMIN \/ ROLE_VENUE_ADMIN \u2192 <span class=\"hljs-string\">'admin'<\/span>\n\u5176\u9918 \u2192 <span class=\"hljs-string\">'user'<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e9%8c%af%e8%aa%a4%e8%99%95%e7%90%86\">\u932f\u8aa4\u8655\u7406<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SSO \u56de\u8abf\u932f\u8aa4<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SSO \u767b\u5165\u5931\u6557\u6642\uff0c\u6703\u4ee5 <code>?error={\u4ee3\u78bc}<\/code> \u8df3\u8f49\u56de\u524d\u7aef\u3002<code>AuthContext<\/code> \u4ee5 Toast \u63d0\u793a\u4f7f\u7528\u8005\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f8c\u7aef\u4ea4\u63db\u932f\u8aa4<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5f8c\u7aef\u56de\u61c9\u683c\u5f0f\uff08\u53c3\u8003\u5f8c\u7aef\u6587\u4ef6\uff09\uff1a<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\">{\n  <span class=\"hljs-attr\">\"errorCode\"<\/span>: <span class=\"hljs-string\">\"SSO_ERROR\"<\/span>,\n  <span class=\"hljs-attr\">\"message\"<\/span>: <span class=\"hljs-string\">\"unauthorized\"<\/span>,\n  <span class=\"hljs-attr\">\"status\"<\/span>: <span class=\"hljs-number\">401<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>message<\/th><th>\u524d\u7aef\u5efa\u8b70\u8655\u7406<\/th><\/tr><\/thead><tbody><tr><td><code>missing_code<\/code><\/td><td>\u63d0\u793a\u300c\u767b\u5165\u8cc7\u8a0a\u4e0d\u5b8c\u6574\uff0c\u8acb\u91cd\u8a66\u300d<\/td><\/tr><tr><td><code>unauthorized<\/code><\/td><td>\u63d0\u793a\u300c\u6388\u6b0a\u78bc\u5df2\u904e\u671f\uff0c\u8acb\u91cd\u65b0\u767b\u5165\u300d<\/td><\/tr><tr><td><code>error_login<\/code><\/td><td>\u63d0\u793a\u300cSSO \u670d\u52d9\u66ab\u6642\u7121\u6cd5\u9023\u7dda\u300d<\/td><\/tr><tr><td>\u5176\u4ed6<\/td><td>\u986f\u793a <code>message<\/code> \u6216\u901a\u7528\u300c\u767b\u5165\u5931\u6557\u300d<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>JWT \u904e\u671f<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u9801\u9762\u8f09\u5165\u6642\uff1a<code>getStoredAuth()<\/code> \u6aa2\u67e5 <code>exp<\/code>\uff0c\u904e\u671f\u5247\u8996\u70ba\u672a\u767b\u5165<\/li>\n\n\n\n<li>API \u8acb\u6c42\u6642\uff1a<code>apiFetch<\/code> \u6536\u5230 <code>401<\/code> \u81ea\u52d5\u6e05\u9664 Token<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u5efa\u8b70\u5f8c\u7e8c\u5728 <code>401<\/code> \u6642\u81ea\u52d5\u89f8\u767c <code>startSsoLogin()<\/code> \u5f15\u5c0e\u91cd\u65b0\u767b\u5165\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e5%ae%89%e5%85%a8%e6%80%a7%e8%80%83%e9%87%8f\">\u5b89\u5168\u6027\u8003\u91cf<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Client Secret \u4e0d\u5f97\u51fa\u73fe\u5728\u524d\u7aef\u7a0b\u5f0f\u78bc\u6216\u74b0\u5883\u8b8a\u6578\u4e2d\u3002<\/strong><\/li>\n\n\n\n<li><strong>\u6388\u6b0a\u78bc\u4e00\u6b21\u6027<\/strong>\uff1a\u53d6\u5f97 <code>code<\/code> \u5f8c\u61c9\u7acb\u5373\u4ea4\u63db\uff0c\u4e26\u4ee5 <code>router.replace<\/code> \u6e05\u9664 URL \u53c3\u6578\u3002<\/li>\n\n\n\n<li><strong>HTTPS<\/strong>\uff1a\u751f\u7522\u74b0\u5883 SSO \u8207 API \u901a\u8a0a\u5fc5\u9808\u4f7f\u7528 HTTPS\u3002<\/li>\n\n\n\n<li><strong>Token \u5132\u5b58<\/strong>\uff1a\u76ee\u524d\u4f7f\u7528 <code>localStorage<\/code> \u5132\u5b58 JWT\u3002\u82e5\u9700\u66f4\u9ad8\u5b89\u5168\u6027\uff0c\u53ef\u6539\u70ba <code>httpOnly<\/code> Cookie\uff08\u9700\u5f8c\u7aef\u914d\u5408 Set-Cookie\uff09\u3002<\/li>\n\n\n\n<li><strong>\u4e0d\u8981\u5728\u65e5\u8a8c\u4e2d\u8f38\u51fa JWT \u6216\u6388\u6b0a\u78bc\u3002<\/strong><\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e6%9c%ac%e5%9c%b0%e6%b8%ac%e8%a9%a6\">\u672c\u5730\u6e2c\u8a66<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u555f\u52d5\u6b65\u9a5f<\/strong><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">npm<\/span> <span class=\"hljs-selector-tag\">install<\/span>\n# \u5efa\u7acb <span class=\"hljs-selector-class\">.env<\/span><span class=\"hljs-selector-class\">.local<\/span> \u4e26\u8a2d\u5b9a <span class=\"hljs-selector-tag\">NEXT_PUBLIC_API_URL<\/span>\n<span class=\"hljs-selector-tag\">npm<\/span> <span class=\"hljs-selector-tag\">run<\/span> <span class=\"hljs-selector-tag\">dev<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6e2c\u8a66\u6d41\u7a0b<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u78ba\u8a8d\u5f8c\u7aef\u5df2\u555f\u52d5\uff0c\u4e14 <code>POST \/api\/auth-tokens\/exchange<\/code> \u53ef\u6b63\u5e38\u904b\u4f5c<\/li>\n\n\n\n<li>\u958b\u555f\u524d\u7aef\uff08\u9810\u8a2d <code>http:\/\/localhost:3000<\/code>\uff09<\/li>\n\n\n\n<li>\u9ede\u64ca\u53f3\u4e0a\u89d2\u300c\u767b\u5165\u300d<\/li>\n\n\n\n<li>\u5b8c\u6210 Google SSO \u767b\u5165<\/li>\n\n\n\n<li>\u78ba\u8a8d\u8df3\u8f49\u56de\u524d\u7aef\u5f8c\u53f3\u4e0a\u89d2\u986f\u793a\u982d\u50cf<\/li>\n\n\n\n<li>\u9ede\u64ca\u982d\u50cf\u9032\u5165 <code>\/profile<\/code>\uff0c\u78ba\u8a8d\u540d\u7a31\u8207\u4fe1\u7bb1\u6b63\u78ba<\/li>\n\n\n\n<li>\u958b\u555f DevTools \u2192 Application \u2192 Local Storage\uff0c\u78ba\u8a8d\u5b58\u5728 <code>authToken<\/code><\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"%e5%b8%b8%e8%a6%8b%e5%95%8f%e9%a1%8c%e8%88%87%e8%a7%a3%e6%b3%95\">\u5e38\u898b\u554f\u984c\u8207\u89e3\u6cd5<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u554f\u984c<\/th><th>\u53ef\u80fd\u539f\u56e0<\/th><th>\u89e3\u6cd5<\/th><\/tr><\/thead><tbody><tr><td>\u767b\u5165\u5f8c\u6c92\u6709 JWT<\/td><td>CORS \u672a\u66b4\u9732 <code>X-Auth-Token<\/code><\/td><td>\u78ba\u8a8d\u5f8c\u7aef CORS <code>exposedHeaders<\/code><\/td><\/tr><tr><td><code>NEXT_PUBLIC_API_URL \u672a\u8a2d\u5b9a<\/code><\/td><td>\u7f3a\u5c11\u74b0\u5883\u8b8a\u6578<\/td><td>\u5efa\u7acb <code>.env.local<\/code><\/td><\/tr><tr><td>\u8df3\u8f49\u5f8c\u51fa\u73fe <code>unauthorized<\/code><\/td><td>code \u5df2\u4f7f\u7528\u6216\u904e\u671f<\/td><td>\u91cd\u65b0\u9ede\u64ca\u767b\u5165<\/td><\/tr><tr><td>\u982d\u50cf\u53ea\u986f\u793a\u9996\u5b57<\/td><td>JWT \u7121 <code>picture<\/code> claim<\/td><td>\u78ba\u8a8d SSO \u56de\u50b3\u8207\u5f8c\u7aef JWT \u7c3d\u767c\u908f\u8f2f<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e5%8e%9f%e5%a7%8b%e7%a2%bc%e7%b4%a2%e5%bc%95\">\u539f\u59cb\u78bc\u7d22\u5f15<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u6a94\u6848<\/th><th>\u8077\u8cac<\/th><\/tr><\/thead><tbody><tr><td><code>lib\/sso.ts<\/code><\/td><td>SSO URL \u5efa\u69cb\u3001code \u4ea4\u63db\u3001JWT \u89e3\u6790\u3001Token \u5b58\u53d6<\/td><\/tr><tr><td><code>lib\/api.ts<\/code><\/td><td>\u9644\u5e36 Bearer Token \u7684 HTTP \u8acb\u6c42<\/td><\/tr><tr><td><code>lib\/hooks\/AuthContext.tsx<\/code><\/td><td>React Context \u8a8d\u8b49\u72c0\u614b\u8207\u56de\u8abf<\/td><\/tr><tr><td><code>components\/layout\/Navbar.tsx<\/code><\/td><td>\u767b\u5165\u6309\u9215\u8207\u4f7f\u7528\u8005\u982d\u50cf UI<\/td><\/tr><tr><td><code>app\/layout.tsx<\/code><\/td><td>Provider \u639b\u8f09\u9ede<\/td><\/tr><tr><td><code>docker-compose.prod.yml<\/code><\/td><td>\u751f\u7522\u74b0\u5883 <code>NEXT_PUBLIC_API_URL<\/code> \u6ce8\u5165<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\" id=\"%e8%88%87%e5%be%8c%e7%ab%af%e6%96%87%e4%bb%b6%e5%b0%8d%e7%85%a7\">\u8207\u5f8c\u7aef\u6587\u4ef6\u5c0d\u7167<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5f8c\u7aef\u6587\u4ef6\u7ae0\u7bc0<\/th><th>\u524d\u7aef\u5c0d\u61c9\u5be6\u4f5c<\/th><\/tr><\/thead><tbody><tr><td>\u00a73 \u6574\u9ad4\u767b\u5165\u6d41\u7a0b<\/td><td><code>AuthContext<\/code> + <code>exchangeCodeForToken()<\/code><\/td><\/tr><tr><td>\u00a77.1 \u767c\u8d77\u767b\u5165<\/td><td><code>buildSsoLoginUrl()<\/code> + <code>startSsoLogin()<\/code><\/td><\/tr><tr><td>\u00a77.2 \u8655\u7406\u56de\u547c<\/td><td><code>AuthProvider<\/code> \u7684 <code>useSearchParams<\/code> effect<\/td><\/tr><tr><td>\u00a77.3 \u4ea4\u63db JWT<\/td><td><code>exchangeCodeForToken()<\/code><\/td><\/tr><tr><td>\u00a77.4 \u53d7\u4fdd\u8b77 API<\/td><td><code>apiFetch()<\/code> \/ <code>apiJson()<\/code><\/td><\/tr><tr><td>\u00a77.5 CORS<\/td><td>\u8b80\u53d6 <code>X-Auth-Token<\/code> Response Header<\/td><\/tr><tr><td>\u00a710 \u5b89\u5168\u6027<\/td><td>\u524d\u7aef\u4e0d\u547c\u53eb <code>\/sso\/verify-code<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>\u672c\u6587\u4ef6\u900f\u904e\u5546\u696d\u667a\u6167\u7814\u7a76\u4e2d\u5fc3 \u2013 \u667a\u6167\u6821\u5712:\u6d3b\u52d5\u7cfb\u7d71\u4f86\u8aaa\u660e\u5546\u667a\u4e2d\u5fc3\u7684SSO\u67b6\u69cb\u3002 \u5546\u667a\u4e2d\u5fc3\u7684SSO\u70ba\u81ea\u67b6\u4f3a\u670d\u5668\uff0c\u975eGoogle SSO\uff0c\u4f46\u539f\u7406\u5927\u540c\u5c0f\u7570\u3002 \u8a72\u6587\u7ae0\u6240\u8ff0\u7a0b\u5f0f\u78bc\u7686\u63d0\u4f9b\u65bc\u6587\u7ae0\u6700\u4e0b\u65b9GitHub\u4e2d\u3002 \u7248\u672c\uff1a0.1.0\u6700\u5f8c\u66f4\u65b0\u6642\u9593\uff1a06\/10\/2026\u6700\u5f8c\u66f4\u65b0\u4eba\u54e1\uff1a\u9673\u6cd3\u6bd3 \u8a2d\u8a08\u539f\u5247 \u539f\u5247 \u8aaa\u660e \u524d\u7aef\u4e0d\u6301\u6709 Client Secret \u524d\u7aef\u7d55\u4e0d\u76f4\u63a5\u547c\u53eb SSO \/sso\/verify-code \u6388\u6b0a\u78bc\u50c5\u7528\u65bc\u4ea4\u63db SSO \u56de\u50b3\u7684 code \u70ba\u4e00\u6b21\u6027\u6388\u6b0a\u78bc\uff0c\u61c9\u7acb\u5373\u8f49\u4ea4\u5f8c\u7aef\u4ea4\u63db JWT JWT \u4f5c\u70ba\u5f8c\u7e8c\u8a8d\u8b49 \u767b\u5165\u5b8c\u6210\u5f8c\uff0c\u6240\u6709\u53d7\u4fdd\u8b77 API \u4ee5 Authorization: Bearer {JWT} \u5b58\u53d6 \u5168\u7ad9\u56de\u8abf\u8655\u7406 SSO \u53ef\u56de\u8df3\u81f3\u4efb\u610f\u9801\u9762\uff0cAuthProvider \u7d71\u4e00\u6514\u622a ?code= \u53c3\u6578 \u7cfb\u7d71\u89d2\u8272 \u89d2\u8272 \u8077\u8cac SSO \u767b\u5165\u4e2d\u5fc3 Google \u7b49 IdP \u767b\u5165\u3001\u6838\u767c\u4e00\u6b21\u6027\u6388\u6b0a\u78bc \u524d\u7aef \u5c0e\u5411 SSO\u3001\u63a5\u6536 code\u3001\u5411\u5f8c\u7aef\u4ea4\u63db JWT\u3001\u5132\u5b58\u4e26\u9644\u5e36 JWT \u547c\u53eb API \u5f8c\u7aef [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":579,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"class_list":["post-569","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/pages\/569","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/comments?post=569"}],"version-history":[{"count":4,"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/pages\/569\/revisions"}],"predecessor-version":[{"id":608,"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/pages\/569\/revisions\/608"}],"up":[{"embeddable":true,"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/pages\/579"}],"wp:attachment":[{"href":"https:\/\/hyc.eshachem.com\/program\/wp-json\/wp\/v2\/media?parent=569"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}