fix(env): fix env

This commit is contained in:
zhangheng
2026-07-14 15:21:28 +08:00
parent d0234effd6
commit c6ab525140
8 changed files with 269 additions and 109 deletions

View File

@@ -1,8 +1,17 @@
# Mock SSO 服务地址(服务端用于 code 换取用户信息 # sso-portal 登录页地址(客户端跳转用,即 OAuth2 授权端点入口
SSO_URL="http://localhost:5501" NEXT_PUBLIC_SSO_URL="https://sso.softtest.cowarobot.com"
# 本应用地址(作为 SSO 回调 app_url # 本应用地址(作为 SSO 回调 client_redirect_uri
NEXT_PUBLIC_APP_URL="http://localhost:4000" NEXT_PUBLIC_APP_URL="http://localhost:4000"
# 注册在 SSO 的应用 IDapp_id # OAuth2 client_id同时作为 URL app_id 传给 sso-portal非敏感可下发前端
SSO_APP_ID="app_demo_001" NEXT_PUBLIC_SSO_APP_ID="6wds5qua3b76hu748zrjmall"
# OAuth2 client_secret仅服务端 /api/callback 调 access_token 用,绝不下发前端)
SSO_CLIENT_SECRET="oXwSxxrRUA0ijJ9N62qWI4oUrVJpLf5F"
# 真实后端 SSO 宿主(服务端调 /api/v1/basis/sso/access_token
SSO_BACKEND_API="https://internal.softtest.cowarobot.com"
# 租户标识
SSO_TENANT="cowarobot"

View File

@@ -1,8 +1,17 @@
# Mock SSO 服务地址(集群内 svc 名,按实际部署填写 # sso-portal 登录页地址(按实际部署填写,客户端跳转用
SSO_URL="http://sso-mock-svc:5501" NEXT_PUBLIC_SSO_URL="http://sso-portal-svc:5501"
# 本应用地址(作为 SSO 回调 app_url,按实际部署填写) # 本应用地址(作为 SSO 回调 client_redirect_uri,按实际部署填写)
NEXT_PUBLIC_APP_URL="" NEXT_PUBLIC_APP_URL=""
# 注册在 SSO 的应用 IDapp_id # OAuth2 client_id同时作为 URL app_id 传给 sso-portal
SSO_APP_ID="app_demo_001" NEXT_PUBLIC_SSO_APP_ID="6wds5qua3b76hu748zrjmall"
# OAuth2 client_secret 含敏感信息,生产建议通过部署平台密钥注入,不入库)
SSO_CLIENT_SECRET="oXwSxxrRUA0ijJ9N62qWI4oUrVJpLf5F"
# 真实后端 SSO 宿主(集群内 svc 名,按实际部署填写)
SSO_BACKEND_API="http://basis-app-svc:6610"
# 租户标识
SSO_TENANT="cowarobot"

View File

@@ -1,8 +1,17 @@
# Mock SSO 服务地址(服务端用于 code 换取用户信息 # sso-portal 登录页地址(客户端跳转用,即 OAuth2 授权端点入口
SSO_URL="http://localhost:5501" NEXT_PUBLIC_SSO_URL="http://localhost:5501"
# 本应用地址(作为 SSO 回调 app_url # 本应用地址(作为 SSO 回调 client_redirect_uri
NEXT_PUBLIC_APP_URL="http://localhost:4000" NEXT_PUBLIC_APP_URL="http://localhost:4000"
# 注册在 SSO 的应用 IDapp_id # OAuth2 client_id同时作为 URL app_id 传给 sso-portal非敏感可下发前端
SSO_APP_ID="app_demo_001" NEXT_PUBLIC_SSO_APP_ID="6wds5qua3b76hu748zrjmall"
# OAuth2 client_secret仅服务端 /api/callback 调 access_token 用,绝不下发前端)
SSO_CLIENT_SECRET="oXwSxxrRUA0ijJ9N62qWI4oUrVJpLf5F"
# 真实后端 SSO 宿主(服务端调 /api/v1/basis/sso/access_token
SSO_BACKEND_API="http://172.16.115.31:6610"
# 租户标识
SSO_TENANT="cowarobot"

View File

@@ -1,6 +1,6 @@
# External AppSSO 接入示例) # External AppSSO 接入示例)
基于 Next.js 的示例应用,演示如何接入 Mock SSO:跳转到 sso-mock 登录 → 在 `/callback` 收到一次性 `sso_code` → 用 code 换取用户信息并写入 Cookie 建立会话。 基于 Next.js 的示例应用,演示如何接入真实 SSOOAuth2 授权码流程):跳转到 sso-portal 登录 → 在 `/callback` 收到一次性 `sso_code`服务端用 code 向后端换 `access_token` 并写入 Cookie 建立会话。
## 快速开始 ## 快速开始
@@ -9,7 +9,9 @@ pnpm install
pnpm dev # http://localhost:4000 pnpm dev # http://localhost:4000
``` ```
> 需同时启动 [`sso-mock`](../sso-mock)(默认 `http://localhost:5501`)。 > 需同时启动 [`sso-portal`](../sso-portal)(默认 `http://localhost:5501`)。
>
> 📘 **接入文档已移至 sso-portal 仓库**[`sso-portal/INTEGRATION_GUIDE.md`](../sso-portal/INTEGRATION_GUIDE.md) —— 通用接入指南(前置准备 / OAuth2 流程 / 环境变量 / 安全 / FAQ / 后端接口速查)。本 README 仅为 external-app 项目自身的快速上手。
## 环境变量 ## 环境变量
@@ -18,22 +20,25 @@ pnpm dev # http://localhost:4000
| 变量 | 端 | 说明 | 示例 | | 变量 | 端 | 说明 | 示例 |
|------|----|------|------| |------|----|------|------|
| `SSO_URL` | 服务端 | Mock SSO 服务地址,`/api/callback` 用它调 `/api/auth/verify` 换用户信息 | dev: `http://localhost:5501`<br>prod: `http://sso-mock-svc:5501` | | `NEXT_PUBLIC_SSO_URL` | 客户端 | sso-portal 登录页地址,跳转 `/login` | dev: `http://localhost:5501`<br>prod: `http://sso-portal-svc:5501` |
| `NEXT_PUBLIC_APP_URL` | 客户端 | 本应用地址,作为 SSO 回调 `app_url` 传给 sso-mock | `http://localhost:4000` | | `NEXT_PUBLIC_APP_URL` | 客户端 | 本应用地址,作为 SSO 回调 `client_redirect_uri` 传给 sso-portal | `http://localhost:4000` |
| `SSO_APP_ID` | 服务端 | 注册在 SSO 的应用 ID`app_id` | `app_demo_001` | | `NEXT_PUBLIC_SSO_APP_ID` | 端 | OAuth2 `client_id`,同时作为 URL `app_id` 传给 sso-portal | `6wds5qua3b76hu748zrjmall` |
| `SSO_CLIENT_SECRET` | 服务端 | OAuth2 `client_secret``/api/callback``access_token` 用,**绝不下发前端** | `oXwSxxrRUA0ijJ9N62qWI4oUrVJpLf5F` |
| `SSO_BACKEND_API` | 服务端 | 真实后端 SSO 宿主,`/api/callback``/api/v1/basis/sso/access_token` 用 | dev: `http://172.16.115.31:6610`<br>prod: `http://basis-app-svc:6610` |
| `SSO_TENANT` | 服务端 | 租户标识SSO 接口的 tenant与 sso-portal 的 `SSO_TENANT` 一致) | `cowarobot` |
> 无密钥,可安全入库。`.env.production` 中的 `SSO_URL` / `NEXT_PUBLIC_APP_URL` 为占位值,请按实际部署填写。 > ⚠️ `SSO_CLIENT_SECRET` 含敏感信息,生产建议通过部署平台密钥注入,不入库;本地私有覆盖写入 `.env*.local`(已被 `.gitignore` 忽略)。`.env.production` 中的 `NEXT_PUBLIC_SSO_URL` / `NEXT_PUBLIC_APP_URL` 为占位值,请按实际部署填写。
## SSO 接入流程 ## SSO 接入流程
1. 应用跳转到 sso-mock`{SSO_URL}/login?app_id={SSO_APP_ID}&app_url={NEXT_PUBLIC_APP_URL}/callback` 1. 应用跳转到 sso-portal`{NEXT_PUBLIC_SSO_URL}/login?app_id={NEXT_PUBLIC_SSO_APP_ID}&app_url={NEXT_PUBLIC_APP_URL}/callback`
2. 用户在 sso-mock 登录(账号 / 手机 / 飞书 2. 用户在 sso-portal 登录(账号 / 手机)
3. sso-mock 回跳:`{app_url}?sso_code=xxxxx` 3. sso-portal 回跳:`{app_url}?sso_code=xxxxx`
4. 本应用 `/callback` 页把 `sso_code` POST 给 `/api/callback` → 服务端调 sso-mock `/api/auth/verify` `{ user, token }` → 写入 Cookie 4. 本应用 `/callback` 页把 `sso_code` POST 给 `/api/callback` → 服务端`client_secret` 调后端 `/api/v1/basis/sso/access_token``access_token` → 写入 httpOnly Cookie
## 接口 ## 接口
| 接口 | 方法 | 说明 | | 接口 | 方法 | 说明 |
|------|------|------| |------|------|------|
| `/api/callback` | POST | 用 `sso_code` 换用户信息并写 Cookie | | `/api/callback` | POST | 用 `sso_code` 向后端换 `access_token` 并写 Cookie |
| `/api/logout` | POST | 清除会话 Cookie | | `/api/logout` | POST | 清除会话 Cookie |

View File

@@ -31,17 +31,41 @@ interface UserInfo {
roles: string[] roles: string[]
} }
export default function DashboardPage({ user }: { user: UserInfo | null }) { // 构建跳转 sso-portal 的登录 URLOAuth2app_id 即 client_id
// 未登录 -> 跳转 SSO const SSO_APP_ID = process.env.NEXT_PUBLIC_SSO_APP_ID || ''
useEffect(() => { const SSO_URL = (process.env.NEXT_PUBLIC_SSO_URL || 'http://localhost:5501').replace(/\/+$/, '')
if (!user) { const APP_URL = (process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:4000').replace(/\/+$/, '')
const appId = 'app_demo_001' const SSO_STATE_STORAGE_KEY = 'sso_login_state'
const callbackUrl = encodeURIComponent('http://localhost:4000/callback')
window.location.href = `http://localhost:5501/login?app_id=${appId}&app_url=${callbackUrl}`
}
}, [user])
if (!user) { function buildSsoLoginUrl(): string {
const bytes = new Uint8Array(32)
window.crypto.getRandomValues(bytes)
const state = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
const loginUrl = new URL(`${SSO_URL}/login`)
window.sessionStorage.setItem(SSO_STATE_STORAGE_KEY, state)
loginUrl.searchParams.set('app_id', SSO_APP_ID)
loginUrl.searchParams.set('app_url', `${APP_URL}/callback`)
loginUrl.searchParams.set('state', state)
return loginUrl.toString()
}
export default function DashboardPage({
user,
authenticated,
}: {
user: UserInfo | null
// 登录态以 token 为准(拿到 access_token 即算成功user 可能为 null
authenticated: boolean
}) {
// 未登录(无 token-> 跳转 SSO
useEffect(() => {
if (!authenticated) {
window.location.href = buildSsoLoginUrl()
}
}, [authenticated])
if (!authenticated) {
return ( return (
<Stack align="center" justify="center" h="100vh"> <Stack align="center" justify="center" h="100vh">
<Text size="lg" c="dimmed"> SSO ...</Text> <Text size="lg" c="dimmed"> SSO ...</Text>
@@ -51,10 +75,8 @@ export default function DashboardPage({ user }: { user: UserInfo | null }) {
const handleLogout = async () => { const handleLogout = async () => {
await fetch('/api/logout', { method: 'POST' }) await fetch('/api/logout', { method: 'POST' })
// 跳到 SSO 登录页(带上 app_id 让 SSO 知道是哪个应用) // 跳到 SSO 登录页;每次登录都生成新的 state 防止登录 CSRF。
const appId = 'app_demo_001' window.location.href = buildSsoLoginUrl()
const callbackUrl = encodeURIComponent('http://localhost:4000/callback')
window.location.href = `http://localhost:5501/login?app_id=${appId}&app_url=${callbackUrl}`
} }
return ( return (
@@ -70,13 +92,15 @@ export default function DashboardPage({ user }: { user: UserInfo | null }) {
<Menu.Target> <Menu.Target>
<Group gap="sm" style={{ cursor: 'pointer' }}> <Group gap="sm" style={{ cursor: 'pointer' }}>
<Avatar color="brand" radius="xl" size="sm"> <Avatar color="brand" radius="xl" size="sm">
{user.name?.[0] || 'U'} {user?.name?.[0] || 'U'}
</Avatar> </Avatar>
<Text size="sm" fw={500}>{user.name}</Text> <Text size="sm" fw={500}>{user?.name || '已登录用户'}</Text>
</Group> </Group>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item leftSection={<IconUser size={14} />}>{user.account}</Menu.Item> <Menu.Item leftSection={<IconUser size={14} />}>
{user?.account || '(未获取到账号)'}
</Menu.Item>
<Menu.Divider /> <Menu.Divider />
<Menu.Item leftSection={<IconLogout size={14} />} color="red" onClick={handleLogout}> <Menu.Item leftSection={<IconLogout size={14} />} color="red" onClick={handleLogout}>
退 退
@@ -89,7 +113,9 @@ export default function DashboardPage({ user }: { user: UserInfo | null }) {
<AppShell.Main> <AppShell.Main>
<Stack maw={800} mx="auto" pt="xl"> <Stack maw={800} mx="auto" pt="xl">
<Text size="xl" fw={700}></Text> <Text size="xl" fw={700}></Text>
<Text c="dimmed"> Mock SSO </Text> <Text c="dimmed">
SSO OAuth2 access_token
</Text>
<SimpleGrid cols={{ base: 1, sm: 2 }} mt="md"> <SimpleGrid cols={{ base: 1, sm: 2 }} mt="md">
<Paper withBorder p="lg" radius="md"> <Paper withBorder p="lg" radius="md">
@@ -97,24 +123,30 @@ export default function DashboardPage({ user }: { user: UserInfo | null }) {
<IconUser size={20} color="#0080FF" /> <IconUser size={20} color="#0080FF" />
<Text fw={600}></Text> <Text fw={600}></Text>
</Group> </Group>
<Stack gap="xs"> {user ? (
<Group> <Stack gap="xs">
<Text size="sm" c="dimmed" w={80}>ID</Text> <Group>
<Code>{user.id}</Code> <Text size="sm" c="dimmed" w={80}>ID</Text>
</Group> <Code>{user.id}</Code>
<Group> </Group>
<Text size="sm" c="dimmed" w={80}></Text> <Group>
<Text size="sm">{user.account}</Text> <Text size="sm" c="dimmed" w={80}></Text>
</Group> <Text size="sm">{user.account}</Text>
<Group> </Group>
<Text size="sm" c="dimmed" w={80}></Text> <Group>
<Text size="sm">{user.name}</Text> <Text size="sm" c="dimmed" w={80}></Text>
</Group> <Text size="sm">{user.name}</Text>
<Group> </Group>
<Text size="sm" c="dimmed" w={80}></Text> <Group>
<Text size="sm">{user.phone}</Text> <Text size="sm" c="dimmed" w={80}></Text>
</Group> <Text size="sm">{user.phone}</Text>
</Stack> </Group>
</Stack>
) : (
<Text size="sm" c="dimmed">
access_token
</Text>
)}
</Paper> </Paper>
<Paper withBorder p="lg" radius="md"> <Paper withBorder p="lg" radius="md">
@@ -122,11 +154,15 @@ export default function DashboardPage({ user }: { user: UserInfo | null }) {
<IconShield size={20} color="#20C997" /> <IconShield size={20} color="#20C997" />
<Text fw={600}></Text> <Text fw={600}></Text>
</Group> </Group>
<Group gap="xs"> {user?.roles?.length ? (
{user.roles?.map((role: string) => ( <Group gap="xs">
<Badge key={role} variant="light">{role}</Badge> {user.roles.map((role: string) => (
))} <Badge key={role} variant="light">{role}</Badge>
</Group> ))}
</Group>
) : (
<Text size="sm" c="dimmed"></Text>
)}
</Paper> </Paper>
</SimpleGrid> </SimpleGrid>
@@ -142,16 +178,16 @@ export default function DashboardPage({ user }: { user: UserInfo | null }) {
<strong>1.</strong> <Code>http://localhost:4000</Code> <strong>1.</strong> <Code>http://localhost:4000</Code>
</Text> </Text>
<Text size="sm"> <Text size="sm">
<strong>2.</strong> Mock SSO<Code>http://localhost:5501/login</Code> <strong>2.</strong> SSO<Code>http://localhost:5501/login</Code>
</Text> </Text>
<Text size="sm"> <Text size="sm">
<strong>3.</strong> SSO SSO <Code>sso_code</Code> <strong>3.</strong> SSO SSO <Code>sso_code</Code>
</Text> </Text>
<Text size="sm"> <Text size="sm">
<strong>4.</strong> <Code>sso_code</Code> <Code>POST /api/auth/verify</Code> <strong>4.</strong> <Code>sso_code</Code> <Code>/api/v1/basis/sso/access_token</Code> access_token
</Text> </Text>
<Text size="sm"> <Text size="sm">
<strong>5.</strong> Cookie <strong>5.</strong> access_token httpOnly Cookie userinfo
</Text> </Text>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,65 +1,138 @@
import { NextResponse } from 'next/server' import { NextResponse } from "next/server";
/** /**
* SSO 回调接口 * SSO 回调接口
* 接收 sso_code调用 Mock SSO 验证接口换取用户信息, *
* 然后将用户信息写入 Cookie。 * OAuth2 授权码流程:用一次性授权 code 向真实后端换取 access_token
* 然后将 token / 用户信息写入 Cookie 建立本地会话。
*
* ⚠️ client_secret 仅在服务端 envSSO_CLIENT_SECRET绝不下发浏览器。
*/ */
const BACKEND = (
process.env.SSO_BACKEND_API || "http://172.16.115.31:6610"
).replace(/\/+$/, "");
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const { code } = await req.json() const { code } = await req.json();
if (!code) { if (!code) {
return NextResponse.json({ error: '缺少 code 参数' }, { status: 400 }) return NextResponse.json({ error: "缺少 code 参数" }, { status: 400 });
} }
// 调用 Mock SSO 的验证接口 const clientId = process.env.NEXT_PUBLIC_SSO_APP_ID;
const ssoUrl = process.env.SSO_URL || 'http://localhost:5501' const clientSecret = process.env.SSO_CLIENT_SECRET;
const verifyRes = await fetch(`${ssoUrl}/api/auth/verify`, { const tenant = process.env.SSO_TENANT || "cowarobot";
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
})
if (!verifyRes.ok) { if (!clientId || !clientSecret) {
const errData = await verifyRes.json().catch(() => ({})) console.error(
"[external-app] 缺少 NEXT_PUBLIC_SSO_APP_ID 或 SSO_CLIENT_SECRET",
);
return NextResponse.json( return NextResponse.json(
{ error: errData.error || 'SSO 验证失败' }, { error: "服务端 SSO 配置缺失" },
{ status: 401 } { status: 500 },
) );
} }
const ssoData = await verifyRes.json() // 用授权 code 换 access_token标准 OAuth2
const { token, refresh_token, user, permissions } = ssoData const tokenRes = await fetch(`${BACKEND}/api/v1/basis/sso/access_token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
code,
grant_type: "authorization_code",
tenant,
}),
});
// 将用户信息和 token 写入 Cookie const data = await tokenRes.json().catch(() => ({}));
const response = NextResponse.json({ success: true }) console.log(
"[external-app] access_token 响应 status:",
tokenRes.status,
"| body:",
JSON.stringify(data).slice(0, 500),
);
response.cookies.set('user', JSON.stringify(user), { if (!tokenRes.ok) {
httpOnly: false, // 客户端需要读取用于展示 const message = data?.message || data?.msg || "换 token 失败";
path: '/', console.error(
maxAge: 86400, "[external-app] access_token error:",
}) tokenRes.status,
message,
);
return NextResponse.json({ error: message }, { status: 401 });
}
response.cookies.set('token', token, { // 兼容平铺 / 外壳两种响应;字段名兼容 user / user_infobasis 习惯用 user_info
const token = data?.access_token ?? data?.data?.access_token;
const refreshToken = data?.refresh_token ?? data?.data?.refresh_token;
if (!token) {
console.error(
"[external-app] access_token 响应中无 token:",
JSON.stringify(data).slice(0, 300),
);
return NextResponse.json(
{ error: "后端未返回 access_token" },
{ status: 502 },
);
}
const infoRes = await fetch(
`${BACKEND}/api/v1/basis/sso/user_info?access_token=${token}`,
{
method: "GET",
},
);
const infoData = await infoRes.json().catch(() => ({}));
const user = infoData?.data;
console.log(
"[external-app] 提取 → token:",
!!token,
"| refresh_token:",
!!refreshToken,
"| user:",
!!user,
user
? `(${Object.keys(user).join(",")})`
: "(无 — 登录态无法建立,首页会跳回 sso-portal)",
);
// 写入 Cookie 建立会话
const response = NextResponse.json({ success: true });
response.cookies.set("token", token, {
httpOnly: true, httpOnly: true,
path: '/', path: "/",
maxAge: 86400, maxAge: 86400,
}) });
if (refresh_token) { if (refreshToken) {
response.cookies.set('refresh_token', refresh_token, { response.cookies.set("refresh_token", refreshToken, {
httpOnly: true, httpOnly: true,
path: '/', path: "/",
maxAge: 86400 * 7, maxAge: 86400 * 7,
}) });
} }
return response if (user) {
response.cookies.set("user", JSON.stringify(user), {
httpOnly: false, // 客户端需要读取用于展示
path: "/",
maxAge: 86400,
});
}
return response;
} catch (err: any) { } catch (err: any) {
console.error("[external-app] callback error:", err?.message);
return NextResponse.json( return NextResponse.json(
{ error: err?.message || '回调处理失败' }, { error: err?.message || "回调处理失败" },
{ status: 500 } { status: 500 },
) );
} }
} }

View File

@@ -1,20 +1,36 @@
'use client' 'use client'
import { Suspense } from 'react' import { Suspense } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation'
import { Stack, Text, Loader } from '@mantine/core' import { Stack, Text, Loader } from '@mantine/core'
const SSO_STATE_STORAGE_KEY = 'sso_login_state'
function CallbackContent() { function CallbackContent() {
const searchParams = useSearchParams() const searchParams = useSearchParams()
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const callbackStarted = useRef(false)
useEffect(() => { useEffect(() => {
if (callbackStarted.current) return
const code = searchParams.get('sso_code') const code = searchParams.get('sso_code')
const state = searchParams.get('state')
const expectedState = window.sessionStorage.getItem(SSO_STATE_STORAGE_KEY)
if (!code) { if (!code) {
setError('缺少 sso_code 参数') setError('缺少 sso_code 参数')
return return
} }
if (!state || !expectedState || state !== expectedState) {
window.sessionStorage.removeItem(SSO_STATE_STORAGE_KEY)
setError('state 无效或已过期,请重新登录')
return
}
callbackStarted.current = true
window.sessionStorage.removeItem(SSO_STATE_STORAGE_KEY)
// 用 code 调用本应用的 /api/callback 换取用户信息 // 用 code 调用本应用的 /api/callback 换取用户信息
fetch('/api/callback', { fetch('/api/callback', {

View File

@@ -3,6 +3,9 @@ import DashboardPage from './DashboardPage'
export default async function Home() { export default async function Home() {
const cookieStore = await cookies() const cookieStore = await cookies()
// 登录态以 token 为准:后端 access_token 接口只返回 token不含用户信息
// 拿到 access_token 即算登录成功user 信息可选(后端暂未提供 userinfo
const token = cookieStore.get('token')?.value
const userCookie = cookieStore.get('user')?.value const userCookie = cookieStore.get('user')?.value
let user = null let user = null
@@ -14,5 +17,5 @@ export default async function Home() {
} }
} }
return <DashboardPage user={user} /> return <DashboardPage user={user} authenticated={!!token} />
} }