From 1822a598660a689ed0c9b266521120ff106e92cb Mon Sep 17 00:00:00 2001 From: well Date: Sun, 20 Oct 2024 20:13:23 +0800 Subject: [PATCH] auth5 --- apps/web2/src/app/(pages)/_app.tsx | 10 -- apps/web2/src/app/(pages)/auth/login/page.tsx | 4 + apps/web2/src/app/(pages)/layout.tsx | 32 ++-- apps/web2/src/app/(pages)/page.tsx | 1 + apps/web2/src/app/(pages)/post/edit/form.tsx | 3 + .../src/app/_components/auth/login-form.tsx | 115 ++++++++++++- .../_components/auth/user-form-validator.ts | 7 +- .../src/app/_components/post/action-form.tsx | 3 + apps/web2/src/app/_components/post/hooks.ts | 6 +- apps/web2/src/app/actions/login.ts | 82 +++++++++ apps/web2/src/app/actions/token.ts | 15 ++ .../src/app/api/auth/[...nextauth]/route.ts | 4 +- apps/web2/src/data/email.ts | 8 + apps/web2/src/data/two-factor-token.ts | 29 ++++ apps/web2/src/data/user.ts | 11 +- .../migrations/20240925025441_1/migration.sql | 12 -- .../20241001005312_add_slug/migration.sql | 13 -- .../20241001025046_test_slug/migration.sql | 3 - .../migration.sql | 18 -- .../migration.sql | 8 - .../20241006023627_postadduser/migration.sql | 14 -- .../migration.sql | 2 - .../20241007014815_107/migration.sql | 99 ----------- .../20241007083718_addhash/migration.sql | 2 - .../20241008073017_123/migration.sql | 158 ------------------ .../20241017063305_frist/migration.sql | 129 ++++++++++++++ .../migration.sql | 8 + .../20241018083752_options_code/migration.sql | 2 + .../database/migrations/migration_lock.toml | 2 +- apps/web2/src/database/schema/schema.prisma | 2 +- .../database/schema/two_factor_token.prisma | 1 + apps/web2/src/lib/redis/client.ts | 6 - apps/web2/src/routes.ts | 2 +- package.json | 3 +- 34 files changed, 429 insertions(+), 385 deletions(-) delete mode 100644 apps/web2/src/app/(pages)/_app.tsx create mode 100644 apps/web2/src/app/(pages)/auth/login/page.tsx create mode 100644 apps/web2/src/app/actions/login.ts create mode 100644 apps/web2/src/data/two-factor-token.ts delete mode 100644 apps/web2/src/database/migrations/20240925025441_1/migration.sql delete mode 100644 apps/web2/src/database/migrations/20241001005312_add_slug/migration.sql delete mode 100644 apps/web2/src/database/migrations/20241001025046_test_slug/migration.sql delete mode 100644 apps/web2/src/database/migrations/20241006021134_add_user_table/migration.sql delete mode 100644 apps/web2/src/database/migrations/20241006022741_changeuseraddpasswd/migration.sql delete mode 100644 apps/web2/src/database/migrations/20241006023627_postadduser/migration.sql delete mode 100644 apps/web2/src/database/migrations/20241006023733_deldefultuserid/migration.sql delete mode 100644 apps/web2/src/database/migrations/20241007014815_107/migration.sql delete mode 100644 apps/web2/src/database/migrations/20241007083718_addhash/migration.sql delete mode 100644 apps/web2/src/database/migrations/20241008073017_123/migration.sql create mode 100644 apps/web2/src/database/migrations/20241017063305_frist/migration.sql create mode 100644 apps/web2/src/database/migrations/20241018051226_two_factor_add_code/migration.sql create mode 100644 apps/web2/src/database/migrations/20241018083752_options_code/migration.sql diff --git a/apps/web2/src/app/(pages)/_app.tsx b/apps/web2/src/app/(pages)/_app.tsx deleted file mode 100644 index dab5c2c..0000000 --- a/apps/web2/src/app/(pages)/_app.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type { AppProps } from 'next/app'; -import { SessionProvider } from 'next-auth/react'; - -export default function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) { - return ( - - ; - - ); -} diff --git a/apps/web2/src/app/(pages)/auth/login/page.tsx b/apps/web2/src/app/(pages)/auth/login/page.tsx new file mode 100644 index 0000000..f312bc0 --- /dev/null +++ b/apps/web2/src/app/(pages)/auth/login/page.tsx @@ -0,0 +1,4 @@ +import LoginForm from '@/app/_components/auth/login-form'; + +const LoginPage: React.FC = () => ; +export default LoginPage; diff --git a/apps/web2/src/app/(pages)/layout.tsx b/apps/web2/src/app/(pages)/layout.tsx index 9212f88..348f628 100644 --- a/apps/web2/src/app/(pages)/layout.tsx +++ b/apps/web2/src/app/(pages)/layout.tsx @@ -1,6 +1,9 @@ import { Metadata } from 'next'; +import { SessionProvider } from 'next-auth/react'; import React, { PropsWithChildren, ReactNode, Suspense } from 'react'; +import { auth } from '@/auth'; + import { Header } from '../_components/header'; import { PageSkeleton } from '../_components/loading/page'; import { Toast, ToastProvider } from '../_components/ui/toast'; @@ -12,16 +15,19 @@ export const metadata: Metadata = { keywords: 'react, next.js, web application', }; -const appLayout: React.FC = ({ children, modal }) => ( - <> -
-
- }>{children} -
- {modal} - - - - -); -export default appLayout; +const AppLayout = async ({ children, modal }: PropsWithChildren & { modal: ReactNode }) => { + const session = await auth(); + return ( + +
+
+ }>{children} +
+ {modal} + + + +
+ ); +}; +export default AppLayout; diff --git a/apps/web2/src/app/(pages)/page.tsx b/apps/web2/src/app/(pages)/page.tsx index 00d0fbf..a3aa80f 100644 --- a/apps/web2/src/app/(pages)/page.tsx +++ b/apps/web2/src/app/(pages)/page.tsx @@ -17,6 +17,7 @@ const App: React.FC<{ searchParams: Record }> = async ({ searchPara isNil(searchParams.page) || Number(searchParams.page) < 1 ? 1 : Number(searchParams.page); const limit = isNil(searchParams.limit) ? 10 : Number(searchParams.limit); const { items, meta } = await queryPostPaginate({ page, limit }); + if (meta.totalPages && meta.totalPages > 0 && page > meta.totalPages) { return redirect('/'); } diff --git a/apps/web2/src/app/(pages)/post/edit/form.tsx b/apps/web2/src/app/(pages)/post/edit/form.tsx index 24872d7..b73be8f 100644 --- a/apps/web2/src/app/(pages)/post/edit/form.tsx +++ b/apps/web2/src/app/(pages)/post/edit/form.tsx @@ -1,11 +1,14 @@ 'use client'; +import { useSession } from 'next-auth/react'; import { useRef } from 'react'; import { PostActionForm } from '@/app/_components/post/action-form'; export const PostCreateForm = () => { const ref = useRef(null); + const session = useSession(); + console.log(session.data.user.name); return ( <> diff --git a/apps/web2/src/app/_components/auth/login-form.tsx b/apps/web2/src/app/_components/auth/login-form.tsx index 8a39bed..998be5d 100644 --- a/apps/web2/src/app/_components/auth/login-form.tsx +++ b/apps/web2/src/app/_components/auth/login-form.tsx @@ -1,17 +1,36 @@ -import { zodResolver } from '@hookform/resolvers/zod'; -import { FC } from 'react'; +'use client'; -import { Form, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useSearchParams } from 'next/navigation'; +import { FC, useState, useTransition } from 'react'; + +import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import { login } from '@/app/actions/login'; import { LoginSchema } from '@/lib/validations /auth'; -import { FormField } from '../ui/form'; +import { Button } from '../ui/button'; +import { Form, FormControl, FormField, FormItem, FormLabel } from '../ui/form'; +import { Input } from '../ui/input'; + +import { SuccessMessage } from './SuccessMessage '; import { CardWrapper } from './card-wrapper'; +import { ErrorMessage } from './error-message'; const LoginForm: FC = () => { + const [showTwoFactor, setShowTwoFactor] = useState(false); + const callbackUrl = useSearchParams().get('callbackUrl'); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [isPending, startTransition] = useTransition(); + const searchParams = useSearchParams(); + const urlError = + searchParams.get('error') === 'OAuthAccountNotLinked' + ? 'Email already in use with different Provider!' + : ''; const form = useForm>({ mode: 'all', defaultValues: { @@ -20,6 +39,27 @@ const LoginForm: FC = () => { }, resolver: zodResolver(LoginSchema), }); + const onSubmit = (data: z.infer) => { + startTransition(() => { + login(data, callbackUrl) + .then((v) => { + if (v.error) { + form.reset(); + setError(v.error); + } + if (v.success) { + form.reset(); + setSuccess(v.success); + } + if (v.twoFactor) { + setShowTwoFactor(true); + } + }) + .catch((e) => { + setError(`Something went wrong: ${e.message}`); + }); + }); + }; return ( { showSocial >
- +
- + {showTwoFactor && ( + <> + ( + + 验证码 + + + + + )} + /> + + )} + {!showTwoFactor && ( + <> + ( + + 邮箱 + + + + + )} + /> + ( + + 密码 + + + + + )} + /> + + )}
+ + +
diff --git a/apps/web2/src/app/_components/auth/user-form-validator.ts b/apps/web2/src/app/_components/auth/user-form-validator.ts index a8d6151..696a812 100644 --- a/apps/web2/src/app/_components/auth/user-form-validator.ts +++ b/apps/web2/src/app/_components/auth/user-form-validator.ts @@ -1,5 +1,5 @@ import { isNil } from 'lodash'; -import { object, optional, string } from 'zod'; +import { object, string } from 'zod'; import { getUserByEmail } from '@/data/user'; @@ -20,8 +20,3 @@ export const registerSchema = object({ .min(3, 'Username must be more than 2 characters') .max(12, 'Username must be less than 12 characters'), }); -export const loginSchema = object({ - email: string().email('填写正确的邮箱格式'), - password: string({ required_error: '密码至少6位' }), - code: optional(string()), -}); diff --git a/apps/web2/src/app/_components/post/action-form.tsx b/apps/web2/src/app/_components/post/action-form.tsx index 804956b..78ce150 100644 --- a/apps/web2/src/app/_components/post/action-form.tsx +++ b/apps/web2/src/app/_components/post/action-form.tsx @@ -2,6 +2,7 @@ import { trim } from 'lodash'; import Link from 'next/link'; +import { useSession } from 'next-auth/react'; import { forwardRef, MouseEventHandler, useEffect, useImperativeHandle, useState } from 'react'; import { generateLowerString } from '@/lib/utils'; @@ -26,12 +27,14 @@ import { usePostActionForm, usePostEditorScreenHandler, usePostFormSubmitHandler import { PostActionFormProps, PostCreateFormRef } from './types'; export const PostActionForm = forwardRef((props, ref) => { + const session = useSession(); // 表单中的数据值获取 const form = usePostActionForm( props.type === 'create' ? { type: props.type } : { type: props.type, post: props.post }, ); const submitHandler = usePostFormSubmitHandler( props.type === 'create' ? { type: 'create' } : { type: 'update', post: props.post }, + session.data.user.id, ); const [body, setBody] = useState(props.type === 'create' ? '正文' : props.post.body); const [sulg, setSulg] = useState(props.type === 'create' ? '' : props.post.slug || ''); diff --git a/apps/web2/src/app/_components/post/hooks.ts b/apps/web2/src/app/_components/post/hooks.ts index 4ce498e..c7060b1 100644 --- a/apps/web2/src/app/_components/post/hooks.ts +++ b/apps/web2/src/app/_components/post/hooks.ts @@ -51,7 +51,7 @@ export function usePostActionForm(params: PostActionFormProps) { }); } -export function usePostFormSubmitHandler(params: PostActionFormProps) { +export function usePostFormSubmitHandler(params: PostActionFormProps, userId: string) { const router = useRouter(); const { toast } = useToast(); const submitHandle = async (data: PostFormData) => { @@ -63,12 +63,14 @@ export function usePostFormSubmitHandler(params: PostActionFormProps) { } try { if (params.type === 'update') { - post = await updatePostItem(params.post.id, data); + post = await updatePostItem(params.post.id, { ...data }); } else { post = await createPostItem({ thumb: `/uploads/thumb/post-${getRandomInt(1, 8)}.png`, + user: { connect: { id: userId } }, ...data, } as PostCreateData); + console.log(post); } // 创建或更新文章后跳转到文章详情页 // 注意,这里不要用push,防止在详情页后退后返回到创建或编辑页面的弹出框 diff --git a/apps/web2/src/app/actions/login.ts b/apps/web2/src/app/actions/login.ts new file mode 100644 index 0000000..041fb17 --- /dev/null +++ b/apps/web2/src/app/actions/login.ts @@ -0,0 +1,82 @@ +'use server'; + +import bcrypt from 'bcrypt'; +import { AuthError } from 'next-auth'; +import { z } from 'zod'; + +import { signIn } from '@/auth'; +import { sendTwoFactorTokenEmail, sendVerificationEmail } from '@/data/email'; +import { getTwoFactorConfirmationByUserId } from '@/data/two-factor-confirmation'; +import { getTwoFactorTokenByEmail } from '@/data/two-factor-token'; +import { getUserByEmail } from '@/data/user'; +import db from '@/lib/db/client'; +import { LoginSchema } from '@/lib/validations /auth'; + +import { DEFAULT_LOGIN_REDIRECT } from '@/routes'; + +import { generateTwoFactorToken, generateVerificationToken } from './token'; + +// 处理用户登录的异步函数,验证用户凭据并实现双因素认证 +export const login = async (values: z.infer, callbackUrl: string | null) => { + const validatedFields = await LoginSchema.safeParseAsync(values); + if (!validatedFields.success) return { error: '登录参数错误!' }; + const { email, password, code } = validatedFields.data; + const existingUser = await getUserByEmail(email); + if (!existingUser || !existingUser.email || !existingUser.password) { + return { error: '邮箱不存在!' }; + } + if (!existingUser.emailVerified) { + const verificationToken = await generateVerificationToken(email); + await sendVerificationEmail({ email, token: verificationToken.token }); + return { success: '验证邮件已发送,请查看收件箱!' }; + } + const passwordMatch = await bcrypt.compare(password, existingUser.password); + if (!passwordMatch) { + return { error: '密码错误!' }; + } + if (existingUser.isTwoFactorEnabled && existingUser.email) { + if (code) { + const twoFactorToken = await getTwoFactorTokenByEmail(email); + if (!twoFactorToken || twoFactorToken.code !== code) return { error: '验证码错误!' }; + + const hasExpired = new Date(twoFactorToken.expires) < new Date(); + if (hasExpired) return { error: '验证码已过期!' }; + await db.twoFactorToken.delete({ where: { id: twoFactorToken.id } }); + getTwoFactorConfirmationByUserId(existingUser.id).then( + async (twoFactorConfirmation) => { + await db.twoFactorConfirmation.delete({ + where: { id: twoFactorConfirmation.id }, + }); + }, + ); + await db.twoFactorConfirmation.create({ + data: { userId: existingUser.id }, + }); + } else { + generateTwoFactorToken(existingUser.id).then(async (v) => { + await sendTwoFactorTokenEmail({ email: existingUser.email, token: v.token }); + }); + return { twoFactor: true }; + } + } else { + try { + await signIn('credentials', { + email: existingUser.email, + password, + redirectTo: callbackUrl || DEFAULT_LOGIN_REDIRECT, + }); + } catch (error) { + if (error instanceof AuthError) { + switch (error.type) { + case 'CredentialsSignin': + return { error: '邮箱或密码错误!' }; + default: + return { error: '登录失败!111' }; + } + } + throw error; + } + } + + return undefined; +}; diff --git a/apps/web2/src/app/actions/token.ts b/apps/web2/src/app/actions/token.ts index 2999472..73543bd 100644 --- a/apps/web2/src/app/actions/token.ts +++ b/apps/web2/src/app/actions/token.ts @@ -1,7 +1,12 @@ +import crypto from 'crypto'; + +import { TwoFactorToken } from '@prisma/client'; import { v7 } from 'uuid'; +import { getTwoFactorTokenByEmail } from '@/data/two-factor-token'; import db from '@/lib/db/client'; +// 生成验证令牌的异步函数,接受邮箱作为参数并返回生成的验证令牌 export const generateVerificationToken = async (email: string) => { const token = v7(); const expires = new Date(Date.now() + 1000 * 60 * 60 * 24); // 24 hours @@ -18,6 +23,7 @@ export const generateVerificationToken = async (email: string) => { }); return verificationToken; }; +// 根据邮箱获取验证令牌的异步函数 export const getVerificationTokenByEmail = async (email: string) => { try { const token = await db.verificationToken.findFirst({ where: { email } }); @@ -26,3 +32,12 @@ export const getVerificationTokenByEmail = async (email: string) => { return null; } }; +// 生成双因子验证令牌的异步函数,接受邮箱作为参数并返回生成的双因子验证令牌 +export const generateTwoFactorToken = async (email: string): Promise => { + const token = crypto.randomInt(100_000, 1_000_000).toString(); + const expires = new Date(Date.now() + 1000 * 60 * 60); // 1 hours + getTwoFactorTokenByEmail(email).then(async (v) => { + await db.twoFactorToken.delete({ where: { id: v.id } }); + }); + return db.twoFactorToken.create({ data: { email, token, expires } }); +}; diff --git a/apps/web2/src/app/api/auth/[...nextauth]/route.ts b/apps/web2/src/app/api/auth/[...nextauth]/route.ts index 0a98352..fae4907 100644 --- a/apps/web2/src/app/api/auth/[...nextauth]/route.ts +++ b/apps/web2/src/app/api/auth/[...nextauth]/route.ts @@ -1,3 +1 @@ -import { handlers } from '@/auth'; - -export const { GET, POST } = handlers; +export { GET, POST } from '@/auth'; diff --git a/apps/web2/src/data/email.ts b/apps/web2/src/data/email.ts index 4d01968..37a1cc8 100644 --- a/apps/web2/src/data/email.ts +++ b/apps/web2/src/data/email.ts @@ -28,3 +28,11 @@ export const sendPasswordResetEmail = async (params: Emailparams) => { html: `

Click here to reset password.

`, }); }; +export const sendTwoFactorTokenEmail = async ({ email, token }: Emailparams) => { + await transporter.sendMail({ + from: '450255477@qq.com', + to: email, + subject: 'Two-factor authentication', + html: `

Your two-factor authentication code is: ${token}

`, + }); +}; diff --git a/apps/web2/src/data/two-factor-token.ts b/apps/web2/src/data/two-factor-token.ts new file mode 100644 index 0000000..3609951 --- /dev/null +++ b/apps/web2/src/data/two-factor-token.ts @@ -0,0 +1,29 @@ +import db from '@/lib/db/client'; + +// 获取二次验证令牌的函数 +export const getTwoFactorTokenByToken = async (token: string) => { + if (!token) { + return Promise.reject(new Error('Token is required')); + } + return db.twoFactorToken + .findUnique({ + where: { token }, + }) + .then((twoFactorToken) => { + if (!twoFactorToken) return Promise.reject(new Error('Token not found')); + return twoFactorToken; + }) + .catch((error) => { + console.error('Error fetching two-factor token:', error); + return Promise.reject(new Error('Unable to retrieve two-factor token')); + }); +}; +export const getTwoFactorTokenByEmail = async (email: string) => { + return db.twoFactorToken + .findFirst({ where: { email } }) + .then((twoFactorToken) => twoFactorToken) + .catch((error) => { + console.log('fun:getTwoFactorTokenByEmail', error); + return Promise.reject(new Error('Unable to retrieve two-factor token')); + }); +}; diff --git a/apps/web2/src/data/user.ts b/apps/web2/src/data/user.ts index ec0a049..b477e4f 100644 --- a/apps/web2/src/data/user.ts +++ b/apps/web2/src/data/user.ts @@ -1,3 +1,5 @@ +'use server'; + import db from '@/lib/db/client'; export const getUserByEmail = async (email: string) => { @@ -9,10 +11,7 @@ export const getUserByEmail = async (email: string) => { } }; export const getUserById = async (id: string) => { - try { - return await db.user.findUnique({ where: { id } }); - } catch (error) { - console.log(error); - return null; - } + const user = await db.user.findUnique({ where: { id } }); + + return user; }; diff --git a/apps/web2/src/database/migrations/20240925025441_1/migration.sql b/apps/web2/src/database/migrations/20240925025441_1/migration.sql deleted file mode 100644 index e58362a..0000000 --- a/apps/web2/src/database/migrations/20240925025441_1/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ --- CreateTable -CREATE TABLE `posts` ( - `id` VARCHAR(191) NOT NULL, - `thumb` VARCHAR(191) NOT NULL, - `title` TEXT NOT NULL, - `summary` TEXT NOT NULL, - `body` TEXT NOT NULL, - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/apps/web2/src/database/migrations/20241001005312_add_slug/migration.sql b/apps/web2/src/database/migrations/20241001005312_add_slug/migration.sql deleted file mode 100644 index e5082e2..0000000 --- a/apps/web2/src/database/migrations/20241001005312_add_slug/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[slug]` on the table `posts` will be added. If there are existing duplicate values, this will fail. - -*/ --- AlterTable -ALTER TABLE `posts` ADD COLUMN `description` VARCHAR(191) NULL, - ADD COLUMN `keywords` VARCHAR(191) NULL, - ADD COLUMN `slug` VARCHAR(191) NULL; - --- CreateIndex -CREATE UNIQUE INDEX `posts_slug_key` ON `posts`(`slug`); diff --git a/apps/web2/src/database/migrations/20241001025046_test_slug/migration.sql b/apps/web2/src/database/migrations/20241001025046_test_slug/migration.sql deleted file mode 100644 index 898f49e..0000000 --- a/apps/web2/src/database/migrations/20241001025046_test_slug/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ --- AlterTable -ALTER TABLE `posts` MODIFY `description` TEXT NULL, - MODIFY `slug` VARCHAR(255) NULL; diff --git a/apps/web2/src/database/migrations/20241006021134_add_user_table/migration.sql b/apps/web2/src/database/migrations/20241006021134_add_user_table/migration.sql deleted file mode 100644 index d5aad0a..0000000 --- a/apps/web2/src/database/migrations/20241006021134_add_user_table/migration.sql +++ /dev/null @@ -1,18 +0,0 @@ --- AlterTable -ALTER TABLE `posts` MODIFY `keywords` VARCHAR(255) NULL; - --- CreateTable -CREATE TABLE `users` ( - `id` VARCHAR(191) NOT NULL, - `name` VARCHAR(191) NOT NULL, - `email` VARCHAR(191) NOT NULL, - `emailVerified` DATETIME(3) NULL, - `image` VARCHAR(191) NULL, - `Post` VARCHAR(191) NULL, - `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - - UNIQUE INDEX `users_name_key`(`name`), - UNIQUE INDEX `users_email_key`(`email`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/apps/web2/src/database/migrations/20241006022741_changeuseraddpasswd/migration.sql b/apps/web2/src/database/migrations/20241006022741_changeuseraddpasswd/migration.sql deleted file mode 100644 index 641a415..0000000 --- a/apps/web2/src/database/migrations/20241006022741_changeuseraddpasswd/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `password` to the `users` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE `users` ADD COLUMN `password` VARCHAR(191) NOT NULL; diff --git a/apps/web2/src/database/migrations/20241006023627_postadduser/migration.sql b/apps/web2/src/database/migrations/20241006023627_postadduser/migration.sql deleted file mode 100644 index e746546..0000000 --- a/apps/web2/src/database/migrations/20241006023627_postadduser/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `Post` on the `users` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE `posts` ADD COLUMN `userId` VARCHAR(255) NOT NULL DEFAULT 'cm1wyshjn000013e4ek229mpu'; - --- AlterTable -ALTER TABLE `users` DROP COLUMN `Post`; - --- AddForeignKey -ALTER TABLE `posts` ADD CONSTRAINT `posts_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/web2/src/database/migrations/20241006023733_deldefultuserid/migration.sql b/apps/web2/src/database/migrations/20241006023733_deldefultuserid/migration.sql deleted file mode 100644 index 96d350a..0000000 --- a/apps/web2/src/database/migrations/20241006023733_deldefultuserid/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE `posts` ALTER COLUMN `userId` DROP DEFAULT; diff --git a/apps/web2/src/database/migrations/20241007014815_107/migration.sql b/apps/web2/src/database/migrations/20241007014815_107/migration.sql deleted file mode 100644 index d8ed6a4..0000000 --- a/apps/web2/src/database/migrations/20241007014815_107/migration.sql +++ /dev/null @@ -1,99 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `created_at` on the `users` table. All the data in the column will be lost. - - You are about to drop the column `emailVerified` on the `users` table. All the data in the column will be lost. - - You are about to drop the column `password` on the `users` table. All the data in the column will be lost. - - You are about to drop the column `updated_at` on the `users` table. All the data in the column will be lost. - - A unique constraint covering the columns `[username]` on the table `users` will be added. If there are existing duplicate values, this will fail. - - Added the required column `updatedAt` to the `users` table without a default value. This is not possible if the table is not empty. - -*/ --- DropIndex -DROP INDEX `users_name_key` ON `users`; - --- AlterTable -ALTER TABLE `users` DROP COLUMN `created_at`, - DROP COLUMN `emailVerified`, - DROP COLUMN `password`, - DROP COLUMN `updated_at`, - ADD COLUMN `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - ADD COLUMN `email_verified` DATETIME(3) NULL, - ADD COLUMN `updatedAt` DATETIME(3) NOT NULL, - ADD COLUMN `username` VARCHAR(191) NULL, - MODIFY `name` VARCHAR(191) NULL, - MODIFY `email` VARCHAR(191) NULL; - --- CreateTable -CREATE TABLE `Account` ( - `id` VARCHAR(191) NOT NULL, - `user_id` VARCHAR(191) NOT NULL, - `type` VARCHAR(191) NOT NULL, - `provider` VARCHAR(191) NOT NULL, - `provider_account_id` VARCHAR(191) NOT NULL, - `refresh_token` TEXT NULL, - `access_token` TEXT NULL, - `expires_at` INTEGER NULL, - `token_type` VARCHAR(191) NULL, - `scope` VARCHAR(191) NULL, - `id_token` TEXT NULL, - `session_state` VARCHAR(191) NULL, - `refresh_token_expires_in` INTEGER NULL, - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - UNIQUE INDEX `Account_user_id_key`(`user_id`), - INDEX `Account_user_id_idx`(`user_id`), - UNIQUE INDEX `Account_provider_provider_account_id_key`(`provider`, `provider_account_id`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `Authenticator` ( - `credentialID` VARCHAR(191) NOT NULL, - `userId` VARCHAR(191) NOT NULL, - `providerAccountId` VARCHAR(191) NOT NULL, - `credentialPublicKey` VARCHAR(191) NOT NULL, - `counter` INTEGER NOT NULL, - `credentialDeviceType` VARCHAR(191) NOT NULL, - `credentialBackedUp` BOOLEAN NOT NULL, - `transports` VARCHAR(191) NULL, - - UNIQUE INDEX `Authenticator_credentialID_key`(`credentialID`), - PRIMARY KEY (`userId`, `credentialID`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `sessions` ( - `id` VARCHAR(191) NOT NULL, - `session_token` VARCHAR(191) NOT NULL, - `user_id` VARCHAR(191) NOT NULL, - `expires` DATETIME(3) NOT NULL, - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - UNIQUE INDEX `sessions_session_token_key`(`session_token`), - INDEX `sessions_user_id_idx`(`user_id`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `verification_tokens` ( - `identifier` VARCHAR(191) NOT NULL, - `token` VARCHAR(191) NOT NULL, - `expires` DATETIME(3) NOT NULL, - - UNIQUE INDEX `verification_tokens_identifier_token_key`(`identifier`, `token`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateIndex -CREATE UNIQUE INDEX `users_username_key` ON `users`(`username`); - --- AddForeignKey -ALTER TABLE `Account` ADD CONSTRAINT `Account_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE `Authenticator` ADD CONSTRAINT `Authenticator_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE `sessions` ADD CONSTRAINT `sessions_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/web2/src/database/migrations/20241007083718_addhash/migration.sql b/apps/web2/src/database/migrations/20241007083718_addhash/migration.sql deleted file mode 100644 index abb2e2f..0000000 --- a/apps/web2/src/database/migrations/20241007083718_addhash/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE `users` ADD COLUMN `hashpassword` VARCHAR(191) NULL; diff --git a/apps/web2/src/database/migrations/20241008073017_123/migration.sql b/apps/web2/src/database/migrations/20241008073017_123/migration.sql deleted file mode 100644 index 014dcc8..0000000 --- a/apps/web2/src/database/migrations/20241008073017_123/migration.sql +++ /dev/null @@ -1,158 +0,0 @@ -/* - Warnings: - - - The primary key for the `users` table will be changed. If it partially fails, the table could be left without primary key constraint. - - You are about to drop the column `createdAt` on the `users` table. All the data in the column will be lost. - - You are about to drop the column `email_verified` on the `users` table. All the data in the column will be lost. - - You are about to drop the column `hashpassword` on the `users` table. All the data in the column will be lost. - - You are about to drop the column `id` on the `users` table. All the data in the column will be lost. - - You are about to drop the column `updatedAt` on the `users` table. All the data in the column will be lost. - - You are about to drop the column `username` on the `users` table. All the data in the column will be lost. - - You are about to drop the column `identifier` on the `verification_tokens` table. All the data in the column will be lost. - - You are about to drop the `Account` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `Authenticator` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `posts` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `sessions` table. If the table is not empty, all the data it contains will be lost. - - A unique constraint covering the columns `[token]` on the table `verification_tokens` will be added. If there are existing duplicate values, this will fail. - - A unique constraint covering the columns `[email,token]` on the table `verification_tokens` will be added. If there are existing duplicate values, this will fail. - - The required column `_id` was added to the `users` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. - - The required column `_id` was added to the `verification_tokens` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. - - Added the required column `email` to the `verification_tokens` table without a default value. This is not possible if the table is not empty. - -*/ --- DropForeignKey -ALTER TABLE `Account` DROP FOREIGN KEY `Account_user_id_fkey`; - --- DropForeignKey -ALTER TABLE `Authenticator` DROP FOREIGN KEY `Authenticator_userId_fkey`; - --- DropForeignKey -ALTER TABLE `posts` DROP FOREIGN KEY `posts_userId_fkey`; - --- DropForeignKey -ALTER TABLE `sessions` DROP FOREIGN KEY `sessions_user_id_fkey`; - --- DropIndex -DROP INDEX `users_username_key` ON `users`; - --- DropIndex -DROP INDEX `verification_tokens_identifier_token_key` ON `verification_tokens`; - --- AlterTable -ALTER TABLE `users` DROP PRIMARY KEY, - DROP COLUMN `createdAt`, - DROP COLUMN `email_verified`, - DROP COLUMN `hashpassword`, - DROP COLUMN `id`, - DROP COLUMN `updatedAt`, - DROP COLUMN `username`, - ADD COLUMN `_id` VARCHAR(191) NOT NULL, - ADD COLUMN `emailVerified` DATETIME(3) NULL, - ADD COLUMN `isTwoFactorEnabled` BOOLEAN NOT NULL DEFAULT false, - ADD COLUMN `password` VARCHAR(191) NULL, - ADD COLUMN `role` ENUM('ADMIN', 'USER') NOT NULL DEFAULT 'USER', - ADD COLUMN `twoFactorConfirmationId` VARCHAR(191) NULL, - ADD PRIMARY KEY (`_id`); - --- AlterTable -ALTER TABLE `verification_tokens` DROP COLUMN `identifier`, - ADD COLUMN `_id` VARCHAR(191) NOT NULL, - ADD COLUMN `email` VARCHAR(191) NOT NULL, - ADD PRIMARY KEY (`_id`); - --- DropTable -DROP TABLE `Account`; - --- DropTable -DROP TABLE `Authenticator`; - --- DropTable -DROP TABLE `posts`; - --- DropTable -DROP TABLE `sessions`; - --- CreateTable -CREATE TABLE `accounts` ( - `_id` VARCHAR(191) NOT NULL, - `userId` VARCHAR(191) NOT NULL, - `type` VARCHAR(191) NOT NULL, - `provider` VARCHAR(191) NOT NULL, - `providerAccountId` VARCHAR(191) NOT NULL, - `refresh_token` VARCHAR(191) NULL, - `access_token` VARCHAR(191) NULL, - `expires_at` INTEGER NULL, - `token_type` VARCHAR(191) NULL, - `scope` VARCHAR(191) NULL, - `id_token` VARCHAR(191) NULL, - `session_state` VARCHAR(191) NULL, - - UNIQUE INDEX `accounts_provider_providerAccountId_key`(`provider`, `providerAccountId`), - PRIMARY KEY (`_id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `password_reset_tokens` ( - `_id` VARCHAR(191) NOT NULL, - `email` VARCHAR(191) NOT NULL, - `token` VARCHAR(191) NOT NULL, - `expires` DATETIME(3) NOT NULL, - - UNIQUE INDEX `password_reset_tokens_token_key`(`token`), - UNIQUE INDEX `password_reset_tokens_email_token_key`(`email`, `token`), - PRIMARY KEY (`_id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `post` ( - `id` VARCHAR(191) NOT NULL, - `thumb` VARCHAR(191) NOT NULL, - `title` TEXT NOT NULL, - `summary` TEXT NOT NULL, - `body` TEXT NOT NULL, - `slug` VARCHAR(255) NULL, - `keywords` VARCHAR(255) NULL, - `description` TEXT NULL, - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - `userId` VARCHAR(255) NOT NULL, - - UNIQUE INDEX `post_slug_key`(`slug`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `two_factor_tokens` ( - `_id` VARCHAR(191) NOT NULL, - `email` VARCHAR(191) NOT NULL, - `token` VARCHAR(191) NOT NULL, - `expires` DATETIME(3) NOT NULL, - - UNIQUE INDEX `two_factor_tokens_token_key`(`token`), - UNIQUE INDEX `two_factor_tokens_email_token_key`(`email`, `token`), - PRIMARY KEY (`_id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `twp_factor_confirmation` ( - `_id` VARCHAR(191) NOT NULL, - `userId` VARCHAR(191) NOT NULL, - - UNIQUE INDEX `twp_factor_confirmation_userId_key`(`userId`), - PRIMARY KEY (`_id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateIndex -CREATE UNIQUE INDEX `verification_tokens_token_key` ON `verification_tokens`(`token`); - --- CreateIndex -CREATE UNIQUE INDEX `verification_tokens_email_token_key` ON `verification_tokens`(`email`, `token`); - --- AddForeignKey -ALTER TABLE `accounts` ADD CONSTRAINT `accounts_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`_id`) ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE `post` ADD CONSTRAINT `post_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`_id`) ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE `twp_factor_confirmation` ADD CONSTRAINT `twp_factor_confirmation_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`_id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/web2/src/database/migrations/20241017063305_frist/migration.sql b/apps/web2/src/database/migrations/20241017063305_frist/migration.sql new file mode 100644 index 0000000..7001150 --- /dev/null +++ b/apps/web2/src/database/migrations/20241017063305_frist/migration.sql @@ -0,0 +1,129 @@ +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER'); + +-- CreateTable +CREATE TABLE "accounts" ( + "_id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "accounts_pkey" PRIMARY KEY ("_id") +); + +-- CreateTable +CREATE TABLE "password_reset_tokens" ( + "_id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "password_reset_tokens_pkey" PRIMARY KEY ("_id") +); + +-- CreateTable +CREATE TABLE "post" ( + "id" TEXT NOT NULL, + "thumb" TEXT NOT NULL, + "title" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "body" TEXT NOT NULL, + "slug" VARCHAR(255), + "keywords" VARCHAR(255), + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "userId" VARCHAR(255) NOT NULL, + + CONSTRAINT "post_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "two_factor_tokens" ( + "_id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "two_factor_tokens_pkey" PRIMARY KEY ("_id") +); + +-- CreateTable +CREATE TABLE "twp_factor_confirmation" ( + "_id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + + CONSTRAINT "twp_factor_confirmation_pkey" PRIMARY KEY ("_id") +); + +-- CreateTable +CREATE TABLE "users" ( + "_id" TEXT NOT NULL, + "name" TEXT, + "email" TEXT, + "emailVerified" TIMESTAMP(3), + "image" TEXT, + "password" TEXT, + "role" "UserRole" NOT NULL DEFAULT 'USER', + "isTwoFactorEnabled" BOOLEAN NOT NULL DEFAULT false, + "twoFactorConfirmationId" TEXT, + + CONSTRAINT "users_pkey" PRIMARY KEY ("_id") +); + +-- CreateTable +CREATE TABLE "verification_tokens" ( + "_id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "verification_tokens_pkey" PRIMARY KEY ("_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "accounts_provider_providerAccountId_key" ON "accounts"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "password_reset_tokens_token_key" ON "password_reset_tokens"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "password_reset_tokens_email_token_key" ON "password_reset_tokens"("email", "token"); + +-- CreateIndex +CREATE UNIQUE INDEX "post_slug_key" ON "post"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "two_factor_tokens_token_key" ON "two_factor_tokens"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "two_factor_tokens_email_token_key" ON "two_factor_tokens"("email", "token"); + +-- CreateIndex +CREATE UNIQUE INDEX "twp_factor_confirmation_userId_key" ON "twp_factor_confirmation"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "verification_tokens_token_key" ON "verification_tokens"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "verification_tokens_email_token_key" ON "verification_tokens"("email", "token"); + +-- AddForeignKey +ALTER TABLE "accounts" ADD CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "post" ADD CONSTRAINT "post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "twp_factor_confirmation" ADD CONSTRAINT "twp_factor_confirmation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("_id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/web2/src/database/migrations/20241018051226_two_factor_add_code/migration.sql b/apps/web2/src/database/migrations/20241018051226_two_factor_add_code/migration.sql new file mode 100644 index 0000000..424edf9 --- /dev/null +++ b/apps/web2/src/database/migrations/20241018051226_two_factor_add_code/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `code` to the `two_factor_tokens` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "two_factor_tokens" ADD COLUMN "code" TEXT NOT NULL; diff --git a/apps/web2/src/database/migrations/20241018083752_options_code/migration.sql b/apps/web2/src/database/migrations/20241018083752_options_code/migration.sql new file mode 100644 index 0000000..e13af68 --- /dev/null +++ b/apps/web2/src/database/migrations/20241018083752_options_code/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "two_factor_tokens" ALTER COLUMN "code" DROP NOT NULL; diff --git a/apps/web2/src/database/migrations/migration_lock.toml b/apps/web2/src/database/migrations/migration_lock.toml index e5a788a..fbffa92 100644 --- a/apps/web2/src/database/migrations/migration_lock.toml +++ b/apps/web2/src/database/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (i.e. Git) -provider = "mysql" \ No newline at end of file +provider = "postgresql" \ No newline at end of file diff --git a/apps/web2/src/database/schema/schema.prisma b/apps/web2/src/database/schema/schema.prisma index b901ac3..7df96a9 100644 --- a/apps/web2/src/database/schema/schema.prisma +++ b/apps/web2/src/database/schema/schema.prisma @@ -10,6 +10,6 @@ generator client { } datasource db { - provider = "mysql" + provider = "postgresql" url = env("DATABASE_URL") } diff --git a/apps/web2/src/database/schema/two_factor_token.prisma b/apps/web2/src/database/schema/two_factor_token.prisma index ef3fbc1..38c0ca4 100644 --- a/apps/web2/src/database/schema/two_factor_token.prisma +++ b/apps/web2/src/database/schema/two_factor_token.prisma @@ -3,6 +3,7 @@ model TwoFactorToken { email String token String @unique expires DateTime + code String? @@unique([email, token]) @@map("two_factor_tokens") diff --git a/apps/web2/src/lib/redis/client.ts b/apps/web2/src/lib/redis/client.ts index 1b68fea..b0990b1 100644 --- a/apps/web2/src/lib/redis/client.ts +++ b/apps/web2/src/lib/redis/client.ts @@ -1,9 +1,3 @@ import { Redis } from 'ioredis'; -import { prisma } from '@/database/client'; - export const redis = new Redis(); -const posts = await prisma.post.findMany({ select: { id: true, slug: true } }); -posts.forEach((post) => { - redis.set(post.id, post.slug); -}); diff --git a/apps/web2/src/routes.ts b/apps/web2/src/routes.ts index f09f0e9..5996411 100644 --- a/apps/web2/src/routes.ts +++ b/apps/web2/src/routes.ts @@ -29,4 +29,4 @@ export const apiAuthPrefix = '/api/auth'; * The default redirect path after loggin in * @type {string} */ -export const DEFAULT_LOGIN_REDIRECT = '/settings'; +export const DEFAULT_LOGIN_REDIRECT = '/'; diff --git a/package.json b/package.json index 77f5424..cca3113 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "web:start": "turbo start --filter=@3rapp/web", "wab:dev": "turbo dev --filter=api --filter=@3rapp/web", "upall": "pnpm up --filter @3rapp/* --latest && pnpm up --latest", - "clean": "rimraf --glob '**/node_modules' && rimraf node_modules" + "clean": "rimraf --glob '**/node_modules' && rimraf node_modules", + "web2:dev": "turbo dev --filter=web2" }, "devDependencies": { "@3rapp/core": "workspace:*",