mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-17 14:33:41 +08:00
init
This commit is contained in:
297
lib/auth.ts
Normal file
297
lib/auth.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import { getServerSession, type NextAuthOptions } from "next-auth";
|
||||
import GitHubProvider from "next-auth/providers/github";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
|
||||
const SECURE_COOKIES:boolean = !!process.env.SECURE_COOKIES;
|
||||
|
||||
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
// debug: !VERCEL_DEPLOYMENT,
|
||||
debug: SECURE_COOKIES,
|
||||
useSecureCookies: SECURE_COOKIES,
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
providers: [
|
||||
GitHubProvider({
|
||||
clientId: process.env.AUTH_GITHUB_ID as string,
|
||||
clientSecret: process.env.AUTH_GITHUB_SECRET as string,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id.toString(),
|
||||
name: profile.name || profile.login,
|
||||
gh_username: profile.login,
|
||||
email: profile.email,
|
||||
image: profile.avatar_url,
|
||||
};
|
||||
},
|
||||
}),
|
||||
CredentialsProvider({
|
||||
// The name to display on the sign in form (e.g. "Sign in with...")
|
||||
name: "Credentials",
|
||||
// `credentials` is used to generate a form on the sign in page.
|
||||
// You can specify which fields should be submitted, by adding keys to the `credentials` object.
|
||||
// e.g. domain, username, password, 2FA token, etc.
|
||||
// You can pass any HTML attribute to the <input> tag through the object.
|
||||
credentials: {
|
||||
username: { label: "Username", type: "text", placeholder: "输入姓名或邮箱" },
|
||||
// password: { label: "Password", type: "password" }
|
||||
},
|
||||
// @ts-ignore
|
||||
async authorize(credential, req) {
|
||||
const username = cleanUpString(`${credential?.username}`);
|
||||
// 验证用户名
|
||||
console.log(credential, username, '==============3')
|
||||
// 判断姓名格式是否符合要求,不符合则拒绝
|
||||
if (username && isName(username)) {
|
||||
// Any object returned will be saved in `user` property of the JWT
|
||||
let user:{[key: string]: string} = {
|
||||
name: username,
|
||||
// email: null
|
||||
}
|
||||
if (isEmail(username)) {
|
||||
user['email'] = username;
|
||||
}
|
||||
await insertUser(user);
|
||||
return user
|
||||
} else {
|
||||
// If you return null then an error will be displayed advising the user to check their details.
|
||||
// return null
|
||||
throw new Error("用户名校验失败")
|
||||
// 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
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
pages: {
|
||||
signIn: `/login`,
|
||||
verifyRequest: `/login`,
|
||||
error: "/login", // Error code passed in query string as ?error=
|
||||
},
|
||||
adapter: PrismaAdapter(prisma),
|
||||
session: { strategy: "jwt", maxAge: 7 * 24 * 60 * 60 },
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: `${SECURE_COOKIES ? "__Secure-" : ""}next-auth.session-token`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
// When working on localhost, the cookie domain must be omitted entirely (https://stackoverflow.com/a/1188145)
|
||||
// domain: VERCEL_DEPLOYMENT
|
||||
// ? `.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`
|
||||
// : undefined,
|
||||
secure: SECURE_COOKIES,
|
||||
},
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
jwt: async ({ token, user }) => {
|
||||
// const current_time = Math.floor(Date.now() / 1000);
|
||||
// console.log('=============', token, user, current_time)
|
||||
if (user) {
|
||||
token.user = user;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
session: async ({ session, token }) => {
|
||||
session.user = {
|
||||
...session.user,
|
||||
// @ts-expect-error
|
||||
id: token.sub,
|
||||
// @ts-expect-error
|
||||
username: token?.user?.username || token?.user?.gh_username,
|
||||
};
|
||||
return session;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getSession() {
|
||||
// console.log('in........',)
|
||||
return getServerSession(authOptions) as Promise<{
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
image: string;
|
||||
};
|
||||
} | null>;
|
||||
}
|
||||
|
||||
// export function withSiteAuth(action: any) {
|
||||
// return async (
|
||||
// formData: FormData | null,
|
||||
// siteId: string,
|
||||
// key: string | null,
|
||||
// ) => {
|
||||
// const session = await getSession();
|
||||
// if (!session) {
|
||||
// return {
|
||||
// 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);
|
||||
// };
|
||||
// }
|
||||
|
||||
|
||||
function isEmail(input: string): boolean {
|
||||
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return emailRegex.test(input);
|
||||
}
|
||||
function isHanZi(input: string): boolean {
|
||||
// 汉字的正则表达式
|
||||
const regChinese = /^[\p{Unified_Ideograph}]+$/u;
|
||||
return regChinese.test(input)
|
||||
}
|
||||
/**
|
||||
* 判断输入的一个字符串是不是拼音
|
||||
* @param input 需要测试的字符串
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isPinYin(input: string): boolean {
|
||||
|
||||
var list = ['a', 'ai', 'an', 'ang', 'ao', 'ba', 'bai', 'ban', 'bang', 'bao', 'bei', 'ben',
|
||||
'beng', 'bi', 'bian', 'biao', 'bie', 'bin', 'bing', 'bo', 'bu', 'ca', 'cai', 'can', 'cang',
|
||||
'cao', 'ce', 'cen', 'ceng', 'cha', 'chai', 'chan', 'chang', 'chao', 'che', 'chen', 'cheng', 'chi',
|
||||
'chong', 'chou', 'chu', 'chua', 'chuai', 'chuan', 'chuang', 'chui', 'chun', 'chuo', 'ci', 'cong',
|
||||
'cou', 'cu', 'cuan', 'cui', 'cun', 'cuo', 'da', 'dai', 'dan', 'dang', 'dao', 'de', 'dei', 'den',
|
||||
'deng', 'di', 'dia', 'dian', 'diao', 'die', 'ding', 'diu', 'dong', 'dou', 'du', 'duan', 'dui', 'dun',
|
||||
'duo', 'e', 'en', 'eng', 'er', 'fa', 'fan', 'fang', 'fei', 'fen', 'feng', 'fiao', 'fo', 'fou', 'fu',
|
||||
'ga', 'gai', 'gan', 'gang', 'gao', 'ge', 'gei', 'gen', 'geng', 'gong', 'gou', 'gu', 'gua', 'guai', 'guan',
|
||||
'guang', 'gui', 'gun', 'guo', 'ha', 'hai', 'han', 'hang', 'hao', 'he', 'hei', 'hen', 'heng', 'hong', 'hou',
|
||||
'hu', 'hua', 'huai', 'huan', 'huang', 'hui', 'hun', 'huo', 'ji', 'jia', 'jian', 'jiang', 'jiao', 'jie',
|
||||
'jin', 'jing', 'jiong', 'jiu', 'ju', 'juan', 'jue', 'ka', 'kai', 'kan', 'kang', 'kao', 'ke', 'ken',
|
||||
'keng', 'kong', 'kou', 'ku', 'kua', 'kuai', 'kuan', 'kuang', 'kui', 'kun', 'kuo', 'la', 'lai', 'lan',
|
||||
'lang', 'lao', 'le', 'lei', 'leng', 'li', 'lia', 'lian', 'liang', 'liao', 'lie', 'lin', 'ling', 'liu',
|
||||
'lo', 'long', 'lou', 'lu', 'luan', 'lun', 'luo', 'lv', 'lve', 'ma', 'mai', 'man', 'mang', 'mao', 'me',
|
||||
'mei', 'men', 'meng', 'mi', 'mian', 'miao', 'mie', 'min', 'ming', 'miu', 'mo', 'mou', 'mu', 'na', 'nai',
|
||||
'nan', 'nang', 'nao', 'ne', 'nei', 'nen', 'neng', 'ni', 'nian', 'niang', 'niao', 'nie', 'nin', 'ning',
|
||||
'niu', 'nong', 'nou', 'nu', 'nuan', 'nun', 'nuo', 'nv', 'nve', 'o', 'ou', 'pa', 'pai', 'pan', 'pang', 'pao',
|
||||
'pei', 'pen', 'peng', 'pi', 'pian', 'piao', 'pie', 'pin', 'ping', 'po', 'pou', 'pu', 'qi', 'qia', 'qian',
|
||||
'qiang', 'qiao', 'qie', 'qin', 'qing', 'qiong', 'qiu', 'qu', 'quan', 'que', 'qun', 'ran', 'rang', 'rao',
|
||||
're', 'ren', 'reng', 'ri', 'rong', 'rou', 'ru', 'rua', 'ruan', 'rui', 'run', 'ruo', 'sa', 'sai', 'san',
|
||||
'sang', 'sao', 'se', 'sen', 'seng', 'sha', 'shai', 'shan', 'shang', 'shao', 'she', 'shei', 'shen', 'sheng',
|
||||
'shi', 'shou', 'shu', 'shua', 'shuai', 'shuan', 'shuang', 'shui', 'shun', 'shuo', 'si', 'song', 'sou',
|
||||
'su', 'suan', 'sui', 'sun', 'suo', 'ta', 'tai', 'tan', 'tang', 'tao', 'te', 'tei', 'teng', 'ti', 'tian',
|
||||
'tiao', 'tie', 'ting', 'tong', 'tou', 'tu', 'tuan', 'tui', 'tun', 'tuo', 'wa', 'wai', 'wan', 'wang',
|
||||
'wei', 'wen', 'weng', 'wo', 'wu', 'xi', 'xia', 'xian', 'xiang', 'xiao', 'xie', 'xin', 'xing', 'xiong',
|
||||
'xiu', 'xu', 'xuan', 'xue', 'xun', 'ya', 'yan', 'yang', 'yao', 'ye', 'yi', 'yin', 'ying', 'yo', 'yong',
|
||||
'you', 'yu', 'yuan', 'yue', 'yun', 'za', 'zai', 'zan', 'zang', 'zao', 'ze', 'zei', 'zen', 'zeng', 'zha',
|
||||
'zhai', 'zhan', 'zhang', 'zhao', 'zhe', 'zhei', 'zhen', 'zheng', 'zhi', 'zhong', 'zhou', 'zhu', 'zhua',
|
||||
'zhuai', 'zhuan', 'zhuang', 'zhui', 'zhun', 'zhuo', 'zi', 'zong', 'zou', 'zu', 'zuan', 'zui', 'zun', 'zuo'];
|
||||
var lowerString = input.toLowerCase();
|
||||
var length = lowerString.length;
|
||||
var index = -1;
|
||||
|
||||
for (var i=0; i<length; i++) {
|
||||
var name = lowerString.substring(0, i+1);
|
||||
index = list.lastIndexOf(name) > index ? list.lastIndexOf(name) : index;
|
||||
}
|
||||
|
||||
// 判断当前 lowerString 是不是拼音(lowerString 在 list 中就是;不在就不是)
|
||||
if (index >= 0) {
|
||||
var item = list[index];
|
||||
lowerString = lowerString.substring(item.length);
|
||||
if (lowerString.length == 0) {
|
||||
return true;
|
||||
} else {
|
||||
return isPinYin(lowerString);
|
||||
// return arguments.callee(lowerString);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function isName(input: string): boolean {
|
||||
return isEmail(input) || isHanZi(input) || isPinYin(input);
|
||||
}
|
||||
|
||||
export async function insertUser(user: {[key: string]: string}) {
|
||||
try {
|
||||
const conditions = [];
|
||||
if (user?.name) {
|
||||
conditions.push({ name: user.name });
|
||||
}
|
||||
if (user?.email) {
|
||||
conditions.push({ email: user.email });
|
||||
}
|
||||
const existingUser = conditions.length? await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: conditions,
|
||||
},
|
||||
}) : null;
|
||||
// console.log('[LOG]', existingUser, user, '=======')
|
||||
if (!existingUser) {
|
||||
const newUser = await prisma.user.create({
|
||||
data: user
|
||||
})
|
||||
// console.log('[LOG]', user, '=======')
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Prisma Error]', e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function cleanUpString(input: string): string {
|
||||
try {
|
||||
// 去除前后空格
|
||||
let cleanedString = input.trim();
|
||||
// 去除非中文、英文、@和.字符
|
||||
cleanedString = cleanedString.replace(/[^\u4e00-\u9fa5a-zA-Z@.]/g, '');
|
||||
return cleanedString;
|
||||
}
|
||||
catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
38
lib/hooks/use-window-size.ts
Normal file
38
lib/hooks/use-window-size.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function useWindowSize() {
|
||||
const [windowSize, setWindowSize] = useState<{
|
||||
width: number | undefined;
|
||||
height: number | undefined;
|
||||
}>({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Handler to call on window resize
|
||||
function handleResize() {
|
||||
// Set window width/height to state
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
// Call handler right away so state gets updated with initial window size
|
||||
handleResize();
|
||||
|
||||
// Remove event listener on cleanup
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []); // Empty array ensures that effect is only run on mount
|
||||
|
||||
return {
|
||||
windowSize,
|
||||
isMobile: typeof windowSize?.width === "number" && windowSize?.width < 768,
|
||||
isDesktop:
|
||||
typeof windowSize?.width === "number" && windowSize?.width >= 768,
|
||||
};
|
||||
}
|
||||
11
lib/prisma.ts
Normal file
11
lib/prisma.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
declare global {
|
||||
var prisma: PrismaClient | undefined;
|
||||
}
|
||||
|
||||
const prisma = global.prisma || new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV === "development") global.prisma = prisma;
|
||||
|
||||
export default prisma;
|
||||
59
lib/utils.ts
Normal file
59
lib/utils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
// export function cn(...inputs: ClassValue[]) {
|
||||
// return twMerge(clsx(inputs));
|
||||
// }
|
||||
export async function fetcher<JSON = any>(
|
||||
input: RequestInfo,
|
||||
init?: RequestInit,
|
||||
): Promise<JSON> {
|
||||
const response = await fetch(input, { ...init, cache: "no-store" });
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export const capitalize = (s: string) => {
|
||||
if (typeof s !== "string") return "";
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
};
|
||||
|
||||
export const truncate = (str: string, num: number) => {
|
||||
if (!str) return "";
|
||||
if (str.length <= num) {
|
||||
return str;
|
||||
}
|
||||
return str.slice(0, num) + "...";
|
||||
};
|
||||
|
||||
export const getBlurDataURL = async (url: string | null) => {
|
||||
if (!url) {
|
||||
return "data:image/webp;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||
}
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://wsrv.nl/?url=${url}&w=50&h=50&blur=5`,
|
||||
);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const base64 = Buffer.from(buffer).toString("base64");
|
||||
|
||||
return `data:image/png;base64,${base64}`;
|
||||
} catch (error) {
|
||||
return "data:image/webp;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||
}
|
||||
};
|
||||
|
||||
export const placeholderBlurhash =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAoJJREFUWEfFl4lu4zAMRO3cx/9/au6reMaOdkxTTl0grQFCRoqaT+SQotq2bV9N8rRt28xms87m83l553eZ/9vr9Wpkz+ezkT0ej+6dv1X81AFw7M4FBACPVn2c1Z3zLgDeJwHgeLFYdAARYioAEAKJEG2WAjl3gCwNYymQQ9b7/V4spmIAwO6Wy2VnAMikBWlDURBELf8CuN1uHQSrPwMAHK5WqwFELQ01AIXdAa7XawfAb3p6AOwK5+v1ugAoEq4FRSFLgavfQ49jAGQpAE5wjgGCeRrGdBArwHOPcwFcLpcGU1X0IsBuN5tNgYhaiFFwHTiAwq8I+O5xfj6fOz38K+X/fYAdb7fbAgFAjIJ6Aav3AYlQ6nfnDoDz0+lUxNiLALvf7XaDNGQ6GANQBKR85V27B4D3QQRw7hGIYlQKWGM79hSweyCUe1blXhEAogfABwHAXAcqSYkxCtHLUK3XBajSc4Dj8dilAeiSAgD2+30BAEKV4GKcAuDqB4TdYwBgPQByCgApUBoE4EJUGvxUjF3Q69/zLw3g/HA45ABKgdIQu+JPIyDnisCfAxAFNFM0EFNQ64gfS0EUoQP8ighrZSjn3oziZEQpauyKbfjbZchHUL/3AS/Dd30gAkxuRACgfO+EWQW8qwI1o+wseNuKcQiESjALvwNoMI0TcRzD4lFcPYwIM+JTF5x6HOs8yI7jeB5oKhpMRFH9UwaSCDB2Jmg4rc6E2TT0biIaG0rQhNqyhpHBcayTTSXH6vcDL7/sdqRK8LkwTsU499E8vRcAojHcZ4AxABdilgrp4lsXk8oVqgwh7+6H3phqd8J0Kk4vbx/+sZqCD/vNLya/5dT9fAH8g1WdNGgwbQAAAABJRU5ErkJggg==";
|
||||
|
||||
export const toDateString = (date: Date) => {
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
export const random = (min: number, max: number) => {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
};
|
||||
Reference in New Issue
Block a user