Compare commits
2 Commits
0149fde0bd
...
6ea8b4d4b9
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ea8b4d4b9 | |||
| 9e9ac373c6 |
66
src/app/(auth)/login/page.tsx
Normal file
66
src/app/(auth)/login/page.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function LoginPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const redirectTo = searchParams.get("redirect");
|
||||
|
||||
const session = authClient.useSession().data;
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
router.push(redirectTo ?? "/profile");
|
||||
}
|
||||
});
|
||||
|
||||
function login() {
|
||||
const username = (document.getElementById("username") as HTMLInputElement).value;
|
||||
const password = (document.getElementById("password") as HTMLInputElement).value;
|
||||
console.log(username, password);
|
||||
if (username.includes("@")) {
|
||||
authClient.signIn.email({
|
||||
email: username,
|
||||
password: username
|
||||
});
|
||||
} else {
|
||||
authClient.signIn.username({
|
||||
username: username,
|
||||
password: password,
|
||||
fetchOptions: {
|
||||
onError: (ctx) => {
|
||||
toast.error(ctx.error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen w-screen">
|
||||
<div className="rounded shadow-lg w-96 flex flex-col py-4">
|
||||
<h1 className="text-6xl m-16 text-center">登录</h1>
|
||||
<input type="text"
|
||||
id="username"
|
||||
placeholder="用户名或邮箱地址"
|
||||
className="mx-auto mb-8 pb-2 w-60 border-b-2 outline-none" />
|
||||
<input type="password"
|
||||
id="password"
|
||||
placeholder="密码"
|
||||
className="mx-auto mb-8 pb-2 w-60 border-b-2 outline-none" />
|
||||
<button
|
||||
onClick={login}
|
||||
className="text-xl rounded shadow w-16 mx-auto p-2 my-4">
|
||||
确认</button>
|
||||
<Link href={"/signup" + (redirectTo ? `?redirect=${redirectTo}` : "")}
|
||||
className="text-center text-blue-800"
|
||||
>没有账号?去注册</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
src/app/(auth)/logout/page.tsx
Normal file
25
src/app/(auth)/logout/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { auth } from "@/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function LogoutPage(
|
||||
props: {
|
||||
searchParams: Promise<{ [key: string]: string | undefined; }>;
|
||||
}
|
||||
) {
|
||||
const searchParams = await props.searchParams;
|
||||
const redirectTo = props.searchParams ?? null;
|
||||
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers()
|
||||
});
|
||||
if (session) {
|
||||
await auth.api.signOut({
|
||||
headers: await headers()
|
||||
});
|
||||
redirect("/login" + (redirectTo ? `?redirect=${redirectTo}` : ""));
|
||||
} else {
|
||||
redirect("/profile");
|
||||
}
|
||||
return (<></>);
|
||||
}
|
||||
13
src/app/(auth)/profile/page.tsx
Normal file
13
src/app/(auth)/profile/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { auth } from "@/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export default async function ProfilePage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
|
||||
if (!session) {
|
||||
redirect("/login?redirect=/profile");
|
||||
}
|
||||
|
||||
redirect(`/users/${session.user.username}`);
|
||||
}
|
||||
67
src/app/(auth)/signup/page.tsx
Normal file
67
src/app/(auth)/signup/page.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function SignUpPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const redirectTo = searchParams.get("redirect");
|
||||
|
||||
const session = authClient.useSession().data;
|
||||
const router = useRouter();
|
||||
|
||||
console.log(JSON.stringify({ re: redirectTo }));
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
router.push(redirectTo ?? "/profile");
|
||||
}
|
||||
});
|
||||
|
||||
function login() {
|
||||
const username = (document.getElementById("username") as HTMLInputElement).value;
|
||||
const email = (document.getElementById("email") as HTMLInputElement).value;
|
||||
const password = (document.getElementById("password") as HTMLInputElement).value;
|
||||
authClient.signUp.email({
|
||||
email: email,
|
||||
name: username,
|
||||
username: username,
|
||||
password: password,
|
||||
fetchOptions: {
|
||||
onError: (ctx) => {
|
||||
toast.error(ctx.error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen w-screen">
|
||||
<div className="rounded shadow-lg w-96 flex flex-col py-4">
|
||||
<h1 className="text-6xl m-16 text-center">注册</h1>
|
||||
<input type="text"
|
||||
id="username"
|
||||
placeholder="用户名"
|
||||
className="mx-auto mb-8 pb-2 w-60 border-b-2 outline-none" />
|
||||
<input type="email"
|
||||
id="email"
|
||||
placeholder="邮箱地址"
|
||||
className="mx-auto mb-8 pb-2 w-60 border-b-2 outline-none" />
|
||||
<input type="password"
|
||||
id="password"
|
||||
placeholder="密码"
|
||||
className="mx-auto mb-8 pb-2 w-60 border-b-2 outline-none" />
|
||||
<button
|
||||
onClick={login}
|
||||
className="text-xl rounded shadow w-16 mx-auto p-2 my-4">
|
||||
确认</button>
|
||||
<Link href={"/login" + (redirectTo ? `?redirect=${redirectTo}` : "")}
|
||||
className="text-center text-blue-800"
|
||||
>已有账号?去登录</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { PageLayout } from "@/components/ui/PageLayout";
|
||||
import { LinkButton } from "@/design-system/base/button";
|
||||
import { LightButton, LinkButton } from "@/design-system/base/button";
|
||||
import { actionGetUserProfileByUsername } from "@/modules/auth/auth-action";
|
||||
import { repoGetFoldersWithTotalPairsByUserId } from "@/modules/folder/folder-repository";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { auth } from "@/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { LogoutButton } from "@/app/users/[username]/LogoutButton";
|
||||
// import { LogoutButton } from "./LogoutButton";
|
||||
|
||||
interface UserPageProps {
|
||||
params: Promise<{ username: string; }>;
|
||||
@@ -42,7 +42,7 @@ export default async function UserPage({ params }: UserPageProps) {
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div></div>
|
||||
{isOwnProfile && <LogoutButton />}
|
||||
{isOwnProfile && <LinkButton href="/logout">登出</LinkButton>}
|
||||
</div>
|
||||
<div className="flex items-center space-x-6">
|
||||
{/* Avatar */}
|
||||
@@ -1,285 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useActionState, startTransition } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { PageLayout } from "@/components/ui/PageLayout";
|
||||
import { Input } from "@/design-system/base/input";
|
||||
import { LightButton, LinkButton } from "@/design-system/base/button";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { actionSignIn, actionSignUp, ActionOutputAuth } from "@/modules/auth/auth-action";
|
||||
|
||||
interface AuthFormProps {
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export function AuthForm({ redirectTo }: AuthFormProps) {
|
||||
const t = useTranslations("auth");
|
||||
const [mode, setMode] = useState<'signin' | 'signup'>('signin');
|
||||
const [clearSignIn, setClearSignIn] = useState(false);
|
||||
const [clearSignUp, setClearSignUp] = useState(false);
|
||||
|
||||
const [signInState, signInActionForm, isSignInPending] = useActionState(
|
||||
async (_prevState: ActionOutputAuth | undefined, formData: FormData) => {
|
||||
if (clearSignIn) {
|
||||
setClearSignIn(false);
|
||||
return undefined;
|
||||
}
|
||||
return actionSignIn(undefined, formData);
|
||||
},
|
||||
undefined
|
||||
);
|
||||
const [signUpState, signUpActionForm, isSignUpPending] = useActionState(
|
||||
async (_prevState: ActionOutputAuth | undefined, formData: FormData) => {
|
||||
if (clearSignUp) {
|
||||
setClearSignUp(false);
|
||||
return undefined;
|
||||
}
|
||||
return actionSignUp(undefined, formData);
|
||||
},
|
||||
undefined
|
||||
);
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const validateForm = (formData: FormData): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
const identifier = formData.get("identifier") as string;
|
||||
const email = formData.get("email") as string;
|
||||
const username = formData.get("username") as string;
|
||||
const password = formData.get("password") as string;
|
||||
const confirmPassword = formData.get("confirmPassword") as string;
|
||||
|
||||
// 登录模式验证
|
||||
if (mode === 'signin') {
|
||||
if (!identifier) {
|
||||
newErrors.identifier = t("identifierRequired");
|
||||
}
|
||||
} else {
|
||||
// 注册模式验证
|
||||
if (!email) {
|
||||
newErrors.email = t("emailRequired");
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
newErrors.email = t("invalidEmail");
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
newErrors.username = t("usernameRequired");
|
||||
} else if (username.length < 3) {
|
||||
newErrors.username = t("usernameTooShort");
|
||||
} else if (!/^[a-zA-Z0-9_]+$/.test(username)) {
|
||||
newErrors.username = t("usernameInvalid");
|
||||
}
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
newErrors.password = t("passwordRequired");
|
||||
} else if (password.length < 8) {
|
||||
newErrors.password = t("passwordTooShort");
|
||||
}
|
||||
|
||||
if (mode === 'signup') {
|
||||
if (!confirmPassword) {
|
||||
newErrors.confirmPassword = t("confirmPasswordRequired");
|
||||
} else if (password !== confirmPassword) {
|
||||
newErrors.confirmPassword = t("passwordsNotMatch");
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
// 基本客户端验证
|
||||
if (!validateForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加 redirectTo 到 formData
|
||||
if (redirectTo) {
|
||||
formData.append("redirectTo", redirectTo);
|
||||
}
|
||||
|
||||
// 使用 startTransition 包装 action 调用
|
||||
startTransition(() => {
|
||||
// 根据模式调用相应的 action
|
||||
if (mode === 'signin') {
|
||||
signInActionForm(formData);
|
||||
} else {
|
||||
signUpActionForm(formData);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleGitHubSignIn = async () => {
|
||||
await authClient.signIn.social({
|
||||
provider: "github",
|
||||
callbackURL: redirectTo || "/"
|
||||
});
|
||||
};
|
||||
|
||||
const currentError = mode === 'signin' ? signInState : signUpState;
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* 页面标题 */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">{t(mode === 'signin' ? 'signIn' : 'signUp')}</h1>
|
||||
</div>
|
||||
|
||||
{/* 服务器端错误提示 */}
|
||||
{currentError?.message && (
|
||||
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
{currentError.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 登录/注册表单 */}
|
||||
<form onSubmit={handleFormSubmit} className="space-y-4">
|
||||
{/* 邮箱/用户名输入(登录模式)或 用户名输入(注册模式) */}
|
||||
{mode === 'signin' ? (
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
name="identifier"
|
||||
placeholder={t("emailOrUsername")}
|
||||
className="w-full px-3 py-2"
|
||||
/>
|
||||
{errors.identifier && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.identifier}</p>
|
||||
)}
|
||||
{currentError?.errors?.email && (
|
||||
<p className="text-red-500 text-sm mt-1">{currentError.errors.email[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 用户名输入(仅注册模式) */}
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder={t("username")}
|
||||
className="w-full px-3 py-2"
|
||||
/>
|
||||
{errors.username && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.username}</p>
|
||||
)}
|
||||
{currentError?.errors?.username && (
|
||||
<p className="text-red-500 text-sm mt-1">{currentError.errors.username[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 邮箱输入(仅注册模式) */}
|
||||
<div>
|
||||
<Input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder={t("email")}
|
||||
className="w-full px-3 py-2"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.email}</p>
|
||||
)}
|
||||
{currentError?.errors?.email && (
|
||||
<p className="text-red-500 text-sm mt-1">{currentError.errors.email[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 密码输入 */}
|
||||
<div>
|
||||
<Input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder={t("password")}
|
||||
className="w-full px-3 py-2"
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.password}</p>
|
||||
)}
|
||||
{currentError?.errors?.password && (
|
||||
<p className="text-red-500 text-sm mt-1">{currentError.errors.password[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 确认密码输入(仅注册模式显示) */}
|
||||
{mode === 'signup' && (
|
||||
<div>
|
||||
<Input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
placeholder={t("confirmPassword")}
|
||||
className="w-full px-3 py-2"
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<LightButton
|
||||
type="submit"
|
||||
className={`w-full py-2 ${isSignInPending || isSignUpPending ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{isSignInPending || isSignUpPending
|
||||
? t("loading")
|
||||
: t(mode === 'signin' ? 'signInButton' : 'signUpButton')
|
||||
}
|
||||
</LightButton>
|
||||
</form>
|
||||
|
||||
{/* 第三方登录区域 */}
|
||||
<div className="mt-6">
|
||||
{/* 分隔线 */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white text-gray-500">或</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GitHub 登录按钮 */}
|
||||
<LightButton
|
||||
onClick={handleGitHubSignIn}
|
||||
className="w-full mt-4 py-2 flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
{t(mode === 'signin' ? 'signInWithGitHub' : 'signUpWithGitHub')}
|
||||
</LightButton>
|
||||
</div>
|
||||
|
||||
{/* 模式切换链接 */}
|
||||
<div className="mt-6 text-center">
|
||||
<LinkButton
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMode(mode === 'signin' ? 'signup' : 'signin');
|
||||
setErrors({});
|
||||
// 清除服务器端错误状态
|
||||
if (mode === 'signin') {
|
||||
setClearSignIn(true);
|
||||
} else {
|
||||
setClearSignUp(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{mode === 'signin'
|
||||
? `${t("noAccount")} ${t("signUp")}`
|
||||
: `${t("hasAccount")} ${t("signIn")}`
|
||||
}
|
||||
</LinkButton>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { auth } from "@/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AuthForm } from "./AuthForm";
|
||||
|
||||
export default async function AuthPage(
|
||||
props: {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined; }>;
|
||||
}
|
||||
) {
|
||||
const searchParams = await props.searchParams;
|
||||
const redirectTo = searchParams.redirect as string | undefined;
|
||||
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (session) {
|
||||
redirect(redirectTo || '/');
|
||||
}
|
||||
|
||||
return <AuthForm redirectTo={redirectTo} />;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { auth } from "@/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export default async function ProfilePage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
|
||||
if (!session) {
|
||||
redirect("/auth?redirect=/profile");
|
||||
}
|
||||
|
||||
// 已登录,跳转到用户资料页面
|
||||
// 优先使用 username,如果没有则使用 email
|
||||
const username = (session.user.username as string) || (session.user.email as string);
|
||||
redirect(`/users/${username}`);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function LogoutButton() {
|
||||
const t = useTranslations("profile");
|
||||
const router = useRouter();
|
||||
return <LightButton onClick={async () => {
|
||||
authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push("/auth?redirect=/profile");
|
||||
}
|
||||
}
|
||||
});
|
||||
}}> {t("logout")}</LightButton >;
|
||||
}
|
||||
@@ -58,8 +58,8 @@ export async function Navbar() {
|
||||
</GhostLightButton>
|
||||
</>
|
||||
|| <>
|
||||
<GhostLightButton href="/auth" className="hidden! md:block!" size="md">{t("sign_in")}</GhostLightButton>
|
||||
<GhostLightButton href="/auth" className="md:hidden! block!" size="md">
|
||||
<GhostLightButton href="/login" className="hidden! md:block!" size="md">{t("sign_in")}</GhostLightButton>
|
||||
<GhostLightButton href="/login" className="md:hidden! block!" size="md">
|
||||
<User size={20} />
|
||||
</GhostLightButton>
|
||||
</>;
|
||||
|
||||
Reference in New Issue
Block a user