Merge pull request #66 from sijinhui/dev

update login
This commit is contained in:
sijinhui 2024-04-18 16:58:26 +08:00 committed by GitHub
commit f6c705cbf7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 325 additions and 165 deletions

View File

@ -51,7 +51,22 @@ async function handle(
}, },
}); });
const count = result.length; const count = result.length;
return NextResponse.json({ count: count, results: result }); return NextResponse.json({
count: count,
results: result.map((item) => {
return {
id: item.id,
name: item.name,
username: item.username,
gh_username: item.gh_username,
image: item.image,
email: item.email,
emailVerified: item.emailVerified,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
};
}),
});
} catch {} } catch {}
return NextResponse.json({ error: "未知错误" }, { status: 500 }); return NextResponse.json({ error: "未知错误" }, { status: 500 });
} }

View File

@ -1,6 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { insertUser } from "@/lib/auth";
import { getTokenLength } from "@/lib/utils"; import { getTokenLength } from "@/lib/utils";
async function handle( async function handle(

View File

@ -1,9 +0,0 @@
input {
text-align: left !important
}
.ant-input {
text-align: left !important
}

View File

@ -1,6 +1,5 @@
"use client"; "use client";
import "./users-table.modules.scss";
import React, { Dispatch, SetStateAction, useEffect, useState } from "react"; import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
import { User } from "@prisma/client"; import { User } from "@prisma/client";
import { import {

View File

@ -3,16 +3,83 @@
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import React, { useState, useEffect, useRef, use } from "react"; import React, { useState, useEffect, useRef, use } from "react";
import { isName } from "@/lib/auth_list"; import { isName } from "@/lib/auth_list";
import { Form, Input, InputRef } from "antd";
import { UserOutlined, MailOutlined } from "@ant-design/icons";
import type { FormProps } from "antd";
import { SignInOptions } from "next-auth/react";
export default function UserLoginButton() { export default function UserLoginButton() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loginForm] = Form.useForm();
const nameInput = useRef<HTMLInputElement>(null); const nameInput = useRef<InputRef>(null);
const passwordInput = useRef<InputRef>(null);
const emailInput = useRef<HTMLInputElement>(null); const emailInput = useRef<HTMLInputElement>(null);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [error, setError] = useState(false); const [password, setPassword] = useState("");
const handleComposition = (e: React.CompositionEvent<HTMLInputElement>) => { const [error, setError] = useState(false);
type FieldType = {
username?: string;
password?: string;
email?: string;
};
const onFinish: FormProps<FieldType>["onFinish"] = (values) => {
setLoading(true);
let signInOptions: SignInOptions = {
redirect: false,
};
let loginProvider = "";
if (values.email) {
loginProvider = "email";
signInOptions = { ...signInOptions, email: values.email };
} else {
loginProvider = "credentials";
signInOptions = {
...signInOptions,
username: values.username,
password: values.password ?? "",
};
}
signIn(loginProvider, signInOptions).then((result) => {
setLoading(false);
if (!result?.error) {
window.location.href =
result?.url && result.url.includes("verify") ? result.url : "/";
} else {
const errorParts = result.error.split(",");
if (errorParts.length > 1) {
loginForm.setFields([
{
name: errorParts[0],
errors: [errorParts[1]],
},
]);
} else {
loginForm.setFields([
{
name: "password",
errors: [result.error],
},
]);
}
}
console.log("response,", result);
});
setLoading(false);
console.log("Success:", values);
};
const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (
errorInfo,
) => {
console.log("Failed:", errorInfo);
};
const handleNameComposition = (
e: React.CompositionEvent<HTMLInputElement>,
) => {
if (e.type === "compositionend") { if (e.type === "compositionend") {
setUsername(e.currentTarget.value); setUsername(e.currentTarget.value);
} }
@ -23,6 +90,12 @@ export default function UserLoginButton() {
} }
setUsername(e.target.value); setUsername(e.target.value);
}; };
const onPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if ((e.nativeEvent as InputEvent).isComposing) {
return;
}
setPassword(e.target.value);
};
const onSubmitHandler = async (e: React.FormEvent<HTMLFormElement>) => { const onSubmitHandler = async (e: React.FormEvent<HTMLFormElement>) => {
// handle yow submition // handle yow submition
setLoading(true); setLoading(true);
@ -40,6 +113,7 @@ export default function UserLoginButton() {
} else { } else {
result = await signIn("credentials", { result = await signIn("credentials", {
username: username, username: username,
password: password,
redirect: false, redirect: false,
}); });
} }
@ -56,10 +130,11 @@ export default function UserLoginButton() {
if (nameInput.current) { if (nameInput.current) {
if (!isName(username)) { if (!isName(username)) {
setError(true); setError(true);
nameInput.current.setCustomValidity("用户名校验失败"); // nameInput
// nameInput.current.setCustomValidity("用户名校验失败");
} else { } else {
setError(false); setError(false);
nameInput.current.setCustomValidity(""); // nameInput.current.setCustomValidity("");
} }
} }
// console.log("username:", username); // console.log("username:", username);
@ -67,79 +142,176 @@ export default function UserLoginButton() {
return ( return (
<> <>
{/* <div className="mt-6 sm:mx-auto sm:w-full sm:max-w-sm">
This example requires updating your template: <Form
```
<html class="h-full bg-white">
<body class="h-full">
```
*/}
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form
className="space-y-6" className="space-y-6"
action="#" // action="#"
method="POST" // method="POST"
autoComplete="off" autoComplete="off"
onSubmit={onSubmitHandler} onFinish={onFinish}
onFinishFailed={onFinishFailed}
size="large"
form={loginForm}
id="login-form"
// onSubmit={onSubmitHandler}
> >
<div> <div>
<div className="mt-2"> <Form.Item
<input name="username"
id="username" rules={[
name="username" {
type="username" validator: async (_, value) => {
ref={nameInput} if (value && !isName(value)) {
// value={username} return Promise.reject(
onCompositionStart={(e) => e.preventDefault()} new Error("Invalid username format!"),
onCompositionEnd={handleComposition} );
onChange={onNameChange} }
const email_value = loginForm.getFieldValue("email");
if (!value && !email_value) {
return Promise.reject(
new Error("Please input your username!"),
);
}
if (value && email_value) {
return Promise.reject(new Error("Field must be unique!"));
}
const password_value = loginForm.getFieldValue("password");
if (!value && password_value) {
return Promise.reject(
new Error("Please input your username!"),
);
}
},
},
]}
>
<Input
// id="basic_username"
// name="username"
// type="username"
// ref={nameInput}
// // value={username}
// onCompositionStart={(e) => e.preventDefault()}
// onCompositionEnd={handleNameComposition}
// onChange={onNameChange}
// required // required
autoComplete="off"
prefix={<UserOutlined style={{ color: "rgba(0,0,0,.25)" }} />}
placeholder="输入姓名、拼音或邮箱" placeholder="输入姓名、拼音或邮箱"
className={`${ className={
loading "text-sm font-medium text-stone-600 dark:text-stone-400"
? "cursor-not-allowed bg-stone-50 dark:bg-stone-800" }
: "bg-white hover:bg-stone-50 active:bg-stone-100 dark:bg-black dark:hover:border-white dark:hover:bg-black" // className={`${
} group my-2 flex h-10 w-full items-center justify-center space-x-2 rounded-md border border-stone-200 transition-colors duration-75 focus:outline-none dark:border-stone-700 // loading
${ // ? "cursor-not-allowed bg-stone-50 dark:bg-stone-800"
error // : "bg-white hover:bg-stone-50 active:bg-stone-100 dark:bg-black dark:hover:border-white dark:hover:bg-black"
? "focus:invalid:border-red-500 focus:invalid:ring-red-500" // } group my-2 flex h-10 w-full items-center justify-center space-x-2 rounded-md border border-stone-200 transition-colors duration-75 focus:outline-none dark:border-stone-700
: "" // ${
} // error
`} // ? "focus:invalid:border-red-500 focus:invalid:ring-red-500"
// : ""
// }
// `}
/> />
<input </Form.Item>
id="email"
name="email" <Form.Item<FieldType>
type="email" // label="Password"
ref={emailInput} name="password"
rules={[
{
validator: async (_, value) => {
if (value) {
if (value.length < 6) {
return Promise.reject(
new Error("Password must be at least 6 characters!"),
);
}
}
},
},
]}
>
<Input.Password
// id="basic_password"
// name="password"
// type="password"
// status={error ? "error" : ""}
// ref={passwordInput}
// value={password}
// // onCompositionStart={(e) => e.preventDefault()}
// // onCompositionEnd={handleComposition}
// onChange={onPasswordChange}
autoComplete="off"
// // required
placeholder="密码验证,测试阶段"
className={
"text-sm font-medium text-stone-600 dark:text-stone-400"
}
// className={`${
// loading
// ? "cursor-not-allowed bg-stone-50 dark:bg-stone-800"
// : "bg-white hover:bg-stone-50 active:bg-stone-100 dark:bg-black dark:hover:border-white dark:hover:bg-black"
// } group my-2 flex h-10 w-full items-center justify-center space-x-2 rounded-md border border-stone-200 transition-colors duration-75 focus:outline-none dark:border-stone-700
// ${
// error
// ? "focus:invalid:border-red-500 focus:invalid:ring-red-500"
// : ""
// }
// `}
/>
</Form.Item>
<Form.Item
name="email"
rules={[
{
type: "email",
message: "The input is not valid E-mail!",
},
{
validator: async (_, value) => {
const username_value = loginForm.getFieldValue("username");
if (value && username_value) {
return Promise.reject(new Error("Field must be unique!"));
}
},
},
]}
>
<Input
// id="email"
// name="email"
// type="email"
// ref={emailInput}
// value={username} // value={username}
onCompositionStart={(e) => e.preventDefault()} // onCompositionStart={(e) => e.preventDefault()}
// onCompositionEnd={handleComposition} // onCompositionEnd={handleComposition}
// onChange={onNameChange} // onChange={onNameChange}
// required // required
prefix={<MailOutlined style={{ color: "rgba(0,0,0,.25)" }} />}
placeholder="邮箱验证,测试阶段" placeholder="邮箱验证,测试阶段"
className={`${ className={
loading "text-sm font-medium text-stone-600 dark:text-stone-400"
? "cursor-not-allowed bg-stone-50 dark:bg-stone-800" }
: "bg-white hover:bg-stone-50 active:bg-stone-100 dark:bg-black dark:hover:border-white dark:hover:bg-black" // className={`${
} group my-2 flex h-10 w-full items-center justify-center space-x-2 rounded-md border border-stone-200 transition-colors duration-75 focus:outline-none dark:border-stone-700 // loading
${ // ? "cursor-not-allowed bg-stone-50 dark:bg-stone-800"
error // : "bg-white hover:bg-stone-50 active:bg-stone-100 dark:bg-black dark:hover:border-white dark:hover:bg-black"
? "focus:invalid:border-red-500 focus:invalid:ring-red-500" // } group my-2 flex h-10 w-full items-center justify-center space-x-2 rounded-md border border-stone-200 transition-colors duration-75 focus:outline-none dark:border-stone-700
: "" // ${
} // error
`} // ? "focus:invalid:border-red-500 focus:invalid:ring-red-500"
// : ""
// }
// `}
/> />
{/*{error && <p className="mt-2 text-pink-600 text-sm">{error}</p>}*/} </Form.Item>
</div>
</div> </div>
<div> <Form.Item>
<button <button
disabled={loading} disabled={loading}
// onClick={(e) => handleSubmit(e)} // onClick={() => loginForm.submit()}
type="submit" type="submit"
className={`${ className={`${
loading loading
@ -149,8 +321,8 @@ export default function UserLoginButton() {
> >
Sign in Sign in
</button> </button>
</div> </Form.Item>
</form> </Form>
</div> </div>
{/*</div>*/} {/*</div>*/}
</> </>

View File

@ -14,7 +14,7 @@
border: 0; border: 0;
border-top: 1px solid var(--color-separator); border-top: 1px solid var(--color-separator);
display: block; display: block;
margin: 2rem auto 1rem; margin: 1rem auto 1rem;
overflow: visible overflow: visible
} }
@ -47,7 +47,7 @@
} }
.signin form input[type],.signin>div input[type] { .signin form input[type],.signin>div input[type] {
margin-bottom: .5rem //margin-bottom: .5rem
} }
.signin form button,.signin>div button { .signin form button,.signin>div button {
@ -73,7 +73,7 @@ html {
.login-form { .login-form {
background-color: #fff; background-color: #fff;
width: 350px; width: 350px;
height: 500px; height: 550px;
border-radius: 15px; border-radius: 15px;
/* 定位到中心 */ /* 定位到中心 */
@ -86,4 +86,12 @@ html {
/* 毛玻璃 */ /* 毛玻璃 */
backdrop-filter: blur(10px); /* 应用模糊效果 */ backdrop-filter: blur(10px); /* 应用模糊效果 */
background-color: rgba(255, 255, 255, 0.5); /* 半透明的白色背景 */ background-color: rgba(255, 255, 255, 0.5); /* 半透明的白色背景 */
} }
input {
text-align: left !important
}
#login-form input:-webkit-autofill {
transition: background-color 5000s ease-in-out 0s;
}

View File

@ -7,7 +7,7 @@ import prisma from "@/lib/prisma";
import { User } from "@prisma/client"; import { User } from "@prisma/client";
import {ADMIN_LIST, isEmail, isName} from "@/lib/auth_list"; import {ADMIN_LIST, isEmail, isName} from "@/lib/auth_list";
import {createTransport} from "nodemailer"; import {createTransport} from "nodemailer";
import { comparePassword, hashPassword } from "@/lib/utils";
const SECURE_COOKIES:boolean = !!process.env.SECURE_COOKIES; const SECURE_COOKIES:boolean = !!process.env.SECURE_COOKIES;
@ -73,13 +73,14 @@ export const authOptions: NextAuthOptions = {
// You can pass any HTML attribute to the <input> tag through the object. // You can pass any HTML attribute to the <input> tag through the object.
credentials: { credentials: {
username: { label: "Username", type: "text", placeholder: "输入姓名或邮箱" }, username: { label: "Username", type: "text", placeholder: "输入姓名或邮箱" },
// password: { label: "Password", type: "password" } password: { label: "Password", type: "password", placeholder: "密码验证,测试阶段" }
}, },
// @ts-ignore // @ts-ignore
async authorize(credential, req) { async authorize(credential, req) {
const username = cleanUpString(`${credential?.username}`); const username = cleanUpString(`${credential?.username}`);
const password = cleanPassword(`${credential?.password}`);
// 验证用户名 // 验证用户名
// console.log(credential, username, '==============3') // console.log(credential, 'p', password, '==============3')
// 判断姓名格式是否符合要求,不符合则拒绝 // 判断姓名格式是否符合要求,不符合则拒绝
if (username && isName(username)) { if (username && isName(username)) {
// Any object returned will be saved in `user` property of the JWT // Any object returned will be saved in `user` property of the JWT
@ -89,11 +90,21 @@ export const authOptions: NextAuthOptions = {
} else { } else {
user['name'] = username; user['name'] = username;
} }
return await insertUser(user) ?? user // 目前用户不存在,则会创建新用户。
let existingUser = await existUser(user); // await insertUser(user)
if (!existingUser) {
// 如果不存在,则报错
// throw new Error("用户查询失败")
// 如果不存在,则创建
existingUser = await insertUser(user);
}
// 有密码就校验密码,没有就直接返回用户
password && validatePassword(password, existingUser.password);
return existingUser;
} else { } else {
// If you return null then an error will be displayed advising the user to check their details. // If you return null then an error will be displayed advising the user to check their details.
// return null // return null
throw new Error("用户名校验失败") throw new Error("username,用户名校验失败")
// You can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter // You can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter
} }
} }
@ -183,93 +194,49 @@ export async function VerifiedAdminUser() {
return !!(name && ADMIN_LIST.includes(name)); return !!(name && ADMIN_LIST.includes(name));
} }
// export function withSiteAuth(action: any) { export function validatePassword(password: string, hashPassword: string | null | undefined ): boolean | void {
// return async ( if (!hashPassword) {
// formData: FormData | null, throw new Error("password,未设置密码");
// siteId: string, }
// key: string | null,
// ) => { if (!comparePassword(password, hashPassword)) {
// const session = await getSession(); throw new Error("password,用户名或密码不正确")
// if (!session) { } else {
// return { return true;
// error: "Not authenticated", }
// }; }
// }
// const site = await prisma.site.findUnique({
// where: {
// id: siteId,
// },
// });
// if (!site || site.userId !== session.user.id) {
// return {
// error: "Not authorized",
// };
// }
//
// return action(formData, site, key);
// };
// }
//
// export function withPostAuth(action: any) {
// return async (
// formData: FormData | null,
// postId: string,
// key: string | null,
// ) => {
// const session = await getSession();
// if (!session?.user.id) {
// return {
// error: "Not authenticated",
// };
// }
// const post = await prisma.post.findUnique({
// where: {
// id: postId,
// },
// include: {
// site: true,
// },
// });
// if (!post || post.userId !== session.user.id) {
// return {
// error: "Post not found",
// };
// }
//
// return action(formData, post, key);
// };
// }
async function existUser(user: {[key: string]: string} | User ) { async function existUser(user: {[key: string]: string} | User ) {
const conditions = []; const conditions = [];
if (user?.name) { if (user?.name) {
conditions.push({ name: user.name }); conditions.push({ name: user.name });
} }
if (user?.email) { if (user?.email) {
conditions.push({ email: user.email }); conditions.push({ email: user.email });
} }
return conditions.length ? await prisma.user.findFirst({ return conditions.length ? await prisma.user.findFirst({
where: { where: {
AND: conditions, AND: conditions,
}, },
}) : null }) : null
} }
export async function insertUser(user: {[key: string]: string}) { export async function insertUser(user: {[key: string]: string}) {
try { try {
const existingUser = await existUser(user); return await prisma.user.create({
// console.log('[LOG]', existingUser, user, '=======') data: user
if (!existingUser) { })
return await prisma.user.create({ // const existingUser = await existUser(user);
data: user // // console.log('[LOG]', existingUser, user, '=======')
}) // if (!existingUser) {
} else { //
// console.log('user==========', existingUser) // } else {
return existingUser; // // console.log('user==========', existingUser)
} // return existingUser;
// }
} catch (e) { } catch (e) {
console.log('[Prisma Error]', e); throw new Error("username,用户创建失败");
return false; // return false;
} }
} }
@ -287,6 +254,15 @@ function cleanUpString(input: string): string {
} }
} }
function cleanPassword(input: string): string {
try {
// 去除前后空格
return input.trim()
}
catch {
return '';
}
}