添加账号系统
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
truncateText,
|
||||
} from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
import { getCaregiverDisplayName } from "@/lib/session";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -38,7 +39,7 @@ export default async function ActivityPage() {
|
||||
currentPath="/activity"
|
||||
title="陪伴动态"
|
||||
description="了解长辈最近的使用情况、聊天内容和智能服务记录。"
|
||||
caregiverToken={caregiver.sessionToken}
|
||||
caregiverLabel={getCaregiverDisplayName(caregiver)}
|
||||
>
|
||||
{usageEvents.length === 0 && conversationTurns.length === 0 && toolCalls.length === 0 ? (
|
||||
<section className="rounded-[32px] border border-dashed border-[var(--line)] bg-white/72 px-6 py-10 text-center shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import {
|
||||
loginCaregiverAccount,
|
||||
sanitizeCaregiverRedirectPath,
|
||||
} from "@/lib/session";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = (await request.json().catch(() => null)) as
|
||||
| {
|
||||
username?: string;
|
||||
password?: string;
|
||||
redirectTo?: string;
|
||||
}
|
||||
| null;
|
||||
|
||||
if (!body?.username || typeof body.username !== "string") {
|
||||
return NextResponse.json({ error: "请先输入用户名。" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof body.password !== "string" || body.password.length === 0) {
|
||||
return NextResponse.json({ error: "请先输入密码。" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await loginCaregiverAccount({
|
||||
username: body.username,
|
||||
password: body.password,
|
||||
userAgent: request.headers.get("user-agent"),
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
redirectTo: sanitizeCaregiverRedirectPath(body.redirectTo),
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "登录失败,请稍后再试。",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { clearCurrentCaregiverSession } from "@/lib/session";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
await clearCurrentCaregiverSession();
|
||||
|
||||
return NextResponse.redirect(new URL("/login", request.url), {
|
||||
status: 303,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import {
|
||||
registerCaregiverAccount,
|
||||
sanitizeCaregiverRedirectPath,
|
||||
} from "@/lib/session";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = (await request.json().catch(() => null)) as
|
||||
| {
|
||||
username?: string;
|
||||
password?: string;
|
||||
redirectTo?: string;
|
||||
}
|
||||
| null;
|
||||
|
||||
if (!body?.username || typeof body.username !== "string") {
|
||||
return NextResponse.json({ error: "请先输入用户名。" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof body.password !== "string" || body.password.length === 0) {
|
||||
return NextResponse.json({ error: "请先输入密码。" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await registerCaregiverAccount({
|
||||
username: body.username,
|
||||
password: body.password,
|
||||
userAgent: request.headers.get("user-agent"),
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
redirectTo: sanitizeCaregiverRedirectPath(body.redirectTo),
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
error instanceof Error ? error.message : "创建账号失败,请稍后再试。",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { bindDeviceToCaregiver, createBindUrl } from "@/lib/monitor-data";
|
||||
import { getOrCreateCaregiverSession } from "@/lib/session";
|
||||
import { getCaregiverSession } from "@/lib/session";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = (await request.json().catch(() => null)) as
|
||||
@@ -18,7 +18,15 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
try {
|
||||
const caregiver = await getOrCreateCaregiverSession();
|
||||
const caregiver = await getCaregiverSession();
|
||||
|
||||
if (!caregiver) {
|
||||
return NextResponse.json(
|
||||
{ error: "请先登录账号后再绑定设备。" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const device = await bindDeviceToCaregiver(caregiver.id, body.deviceUuid);
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { createFamilyMessageFromCaregiver } from "@/lib/monitor-data";
|
||||
import { getOrCreateCaregiverSession } from "@/lib/session";
|
||||
import { getCaregiverSession } from "@/lib/session";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = (await request.json().catch(() => null)) as
|
||||
@@ -27,7 +27,15 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
try {
|
||||
const caregiver = await getOrCreateCaregiverSession();
|
||||
const caregiver = await getCaregiverSession();
|
||||
|
||||
if (!caregiver) {
|
||||
return NextResponse.json(
|
||||
{ error: "请先登录账号后再发送留言。" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const message = await createFamilyMessageFromCaregiver({
|
||||
caregiverId: caregiver.id,
|
||||
rawDeviceValue: body.deviceUuid,
|
||||
|
||||
@@ -1,49 +1,19 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import {
|
||||
CAREGIVER_SESSION_COOKIE,
|
||||
createCaregiverSessionCookieValue,
|
||||
buildCaregiverLoginPath,
|
||||
getCaregiverSession,
|
||||
sanitizeCaregiverRedirectPath,
|
||||
} from "@/lib/session";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
function normalizeRedirectPath(redirectTo?: string | null) {
|
||||
if (!redirectTo || !redirectTo.startsWith("/") || redirectTo.startsWith("//")) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
return redirectTo;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const redirectTo = normalizeRedirectPath(
|
||||
const redirectTo = sanitizeCaregiverRedirectPath(
|
||||
request.nextUrl.searchParams.get("redirectTo"),
|
||||
);
|
||||
const existingCaregiver = await getCaregiverSession();
|
||||
const response = new NextResponse(null, {
|
||||
const caregiver = await getCaregiverSession();
|
||||
const location = caregiver ? redirectTo : buildCaregiverLoginPath(redirectTo);
|
||||
|
||||
return NextResponse.redirect(new URL(location, request.url), {
|
||||
status: 307,
|
||||
headers: {
|
||||
Location: redirectTo,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCaregiver) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { sessionToken, options } = createCaregiverSessionCookieValue();
|
||||
|
||||
await prisma.caregiverAccount.upsert({
|
||||
where: { sessionToken },
|
||||
update: {},
|
||||
create: { sessionToken },
|
||||
});
|
||||
|
||||
response.cookies.set({
|
||||
name: CAREGIVER_SESSION_COOKIE,
|
||||
value: sessionToken,
|
||||
...options,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getOrCreateCaregiverSession } from "@/lib/session";
|
||||
import { getCaregiverSession } from "@/lib/session";
|
||||
import { savePushSubscription } from "@/lib/push";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
@@ -50,7 +50,15 @@ export async function POST(request: Request) {
|
||||
};
|
||||
|
||||
try {
|
||||
const caregiver = await getOrCreateCaregiverSession();
|
||||
const caregiver = await getCaregiverSession();
|
||||
|
||||
if (!caregiver) {
|
||||
return NextResponse.json(
|
||||
{ error: "请先登录账号后再开启消息提醒。" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
await savePushSubscription({
|
||||
caregiverId: caregiver.id,
|
||||
subscription,
|
||||
|
||||
+44
-3
@@ -3,7 +3,7 @@ import { redirect } from "next/navigation";
|
||||
|
||||
import { bindDeviceToCaregiver } from "@/lib/monitor-data";
|
||||
import {
|
||||
buildCaregiverSessionBootstrapPath,
|
||||
buildCaregiverLoginPath,
|
||||
getCaregiverSession,
|
||||
} from "@/lib/session";
|
||||
|
||||
@@ -58,13 +58,54 @@ export default async function BindPage({ searchParams }: BindPageProps) {
|
||||
|
||||
if (!caregiver) {
|
||||
redirect(
|
||||
buildCaregiverSessionBootstrapPath(
|
||||
buildCaregiverLoginPath(
|
||||
`/bind?deviceUuid=${encodeURIComponent(rawDeviceValue)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const device = await bindDeviceToCaregiver(caregiver.id, rawDeviceValue);
|
||||
let device: Awaited<ReturnType<typeof bindDeviceToCaregiver>> | null = null;
|
||||
let bindError: string | null = null;
|
||||
|
||||
try {
|
||||
device = await bindDeviceToCaregiver(caregiver.id, rawDeviceValue);
|
||||
} catch (error) {
|
||||
bindError =
|
||||
error instanceof Error ? error.message : "绑定失败,请稍后再试。";
|
||||
}
|
||||
|
||||
if (!device) {
|
||||
return (
|
||||
<main className="mx-auto flex min-h-screen max-w-3xl items-center px-4 py-8 sm:px-6">
|
||||
<section className="w-full rounded-[36px] border border-white/70 bg-white/84 p-8 shadow-[0_30px_70px_rgba(115,76,42,0.14)] backdrop-blur">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
暂时没连上
|
||||
</p>
|
||||
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
|
||||
这次还没能完成设备连接
|
||||
</h1>
|
||||
<p className="mt-4 text-base leading-8 text-[var(--muted)]">
|
||||
{bindError || "请重新扫码,或请长辈重新打开设备连接页面后再试一次。"}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/scan"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)]"
|
||||
>
|
||||
重新扫码
|
||||
</Link>
|
||||
<Link
|
||||
href="/devices"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-white px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
|
||||
>
|
||||
返回设备页
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex min-h-screen max-w-3xl items-center px-4 py-8 sm:px-6">
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
truncateText,
|
||||
} from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
import { getCaregiverDisplayName } from "@/lib/session";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -42,7 +43,7 @@ export default async function DeviceDetailPage({ params }: DeviceDetailPageProps
|
||||
currentPath="/devices"
|
||||
title={getDeviceName(binding.elderDevice.displayName)}
|
||||
description="给长辈写问候、查看留言和最近的使用动态。"
|
||||
caregiverToken={caregiver.sessionToken}
|
||||
caregiverLabel={getCaregiverDisplayName(caregiver)}
|
||||
>
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PwaControls } from "@/components/pwa-controls";
|
||||
import { getCaregiverDevices } from "@/lib/caregiver-panel";
|
||||
import { formatDateTime, getDeviceName, truncateText } from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
import { getCaregiverDisplayName } from "@/lib/session";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -18,7 +19,7 @@ export default async function DevicesPage() {
|
||||
currentPath="/devices"
|
||||
title="我的设备"
|
||||
description="管理已连接的长辈设备,点击进入设备详情查看留言和使用动态。"
|
||||
caregiverToken={caregiver.sessionToken}
|
||||
caregiverLabel={getCaregiverDisplayName(caregiver)}
|
||||
actions={<PwaControls />}
|
||||
>
|
||||
<section className="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { AccountAuthForm } from "@/components/account-auth-form";
|
||||
import {
|
||||
getCaregiverSession,
|
||||
sanitizeCaregiverRedirectPath,
|
||||
} from "@/lib/session";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type LoginPageProps = {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
};
|
||||
|
||||
export default async function LoginPage({ searchParams }: LoginPageProps) {
|
||||
const caregiver = await getCaregiverSession();
|
||||
const params = await searchParams;
|
||||
const redirectTo = sanitizeCaregiverRedirectPath(
|
||||
typeof params.redirectTo === "string" ? params.redirectTo : null,
|
||||
);
|
||||
|
||||
if (caregiver) {
|
||||
redirect(redirectTo);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex min-h-screen max-w-6xl items-center px-4 py-8 sm:px-6 lg:px-8">
|
||||
<section className="grid w-full gap-6 lg:grid-cols-[0.95fr_1.05fr]">
|
||||
<div className="relative overflow-hidden rounded-[40px] border border-white/75 bg-[linear-gradient(160deg,rgba(53,84,141,0.95),rgba(28,45,80,0.94))] p-7 text-white shadow-[0_36px_90px_rgba(53,84,141,0.24)] sm:p-8">
|
||||
<div className="absolute -right-10 top-[-40px] h-44 w-44 rounded-full bg-white/10 blur-2xl" />
|
||||
<div className="absolute bottom-[-48px] left-[-12px] h-40 w-40 rounded-full border border-white/16" />
|
||||
|
||||
<p className="relative text-sm font-semibold tracking-[0.22em] text-white/78 uppercase">
|
||||
家人连线
|
||||
</p>
|
||||
<h1 className="relative mt-5 font-[family-name:var(--font-display)] text-4xl leading-[1.18] sm:text-5xl">
|
||||
用同一个账号,
|
||||
<br />
|
||||
在每一台家属设备上继续守护
|
||||
</h1>
|
||||
<p className="relative mt-5 max-w-2xl text-base leading-8 text-white/78 sm:text-lg">
|
||||
现在开始,设备连接、留言记录和消息提醒都按账号归属。您在手机、平板或电脑上登录同一个账号,长辈发来的新留言都会一起送达。
|
||||
</p>
|
||||
|
||||
<div className="relative mt-8 grid gap-3">
|
||||
{[
|
||||
"绑定过的长辈设备会跟随账号保留,不再分散到不同浏览器会话。",
|
||||
"一台设备开了提醒,另一台设备也登录同一个账号时,同样可以独立接收推送。",
|
||||
"登录后会自动回到刚才的页面,扫码和绑定流程不用重新开始。",
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="rounded-[22px] border border-white/12 bg-white/8 px-5 py-4 text-sm leading-7 text-white/84"
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div className="rounded-[32px] border border-white/75 bg-white/80 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)] sm:p-7">
|
||||
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
|
||||
账号登录
|
||||
</p>
|
||||
<h2 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
|
||||
先登录,再继续刚才的操作
|
||||
</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-[var(--muted)]">
|
||||
如果您刚刚在扫码或添加设备,登录完成后会自动回到原来的页面,不需要重新输入。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AccountAuthForm redirectTo={redirectTo} />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getMessageStatusLabel,
|
||||
} from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
import { getCaregiverDisplayName } from "@/lib/session";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -38,7 +39,7 @@ export default async function MessageDetailPage({ params }: MessageDetailPagePro
|
||||
currentPath="/messages"
|
||||
title={`留言 #${message.publicId}`}
|
||||
description="查看这条留言的完整内容和设备信息。"
|
||||
caregiverToken={caregiver.sessionToken}
|
||||
caregiverLabel={getCaregiverDisplayName(caregiver)}
|
||||
>
|
||||
<section className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
|
||||
<article className="rounded-[32px] border border-white/75 bg-white/82 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)]">
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
truncateText,
|
||||
} from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
import { getCaregiverDisplayName } from "@/lib/session";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -33,7 +34,7 @@ export default async function MessagesPage() {
|
||||
currentPath="/messages"
|
||||
title="留言板"
|
||||
description="长辈捆来的话和您发出的问候,都在这里。"
|
||||
caregiverToken={caregiver.sessionToken}
|
||||
caregiverLabel={getCaregiverDisplayName(caregiver)}
|
||||
actions={<PwaControls />}
|
||||
>
|
||||
<section className="grid gap-4 md:grid-cols-3">
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ import {
|
||||
truncateText,
|
||||
} from "@/lib/panel-format";
|
||||
import { requireCaregiverSession } from "@/lib/page-auth";
|
||||
import { getCaregiverDisplayName } from "@/lib/session";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -38,7 +39,7 @@ export default async function Home() {
|
||||
currentPath="/"
|
||||
title="今日概览"
|
||||
description="随时了解长辈的近况,留言和动态一目了然。"
|
||||
caregiverToken={dashboard.caregiver.sessionToken}
|
||||
caregiverLabel={getCaregiverDisplayName(dashboard.caregiver)}
|
||||
actions={<PwaControls />}
|
||||
>
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
|
||||
Reference in New Issue
Block a user