master
well 2024-10-20 20:13:23 +08:00
parent 22dfa0204e
commit 1822a59866
34 changed files with 429 additions and 385 deletions

View File

@ -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 (
<SessionProvider session={session}>
<Component {...pageProps} />;
</SessionProvider>
);
}

View File

@ -0,0 +1,4 @@
import LoginForm from '@/app/_components/auth/login-form';
const LoginPage: React.FC = () => <LoginForm />;
export default LoginPage;

View File

@ -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,8 +15,10 @@ export const metadata: Metadata = {
keywords: 'react, next.js, web application',
};
const appLayout: React.FC<PropsWithChildren & { modal: ReactNode }> = ({ children, modal }) => (
<>
const AppLayout = async ({ children, modal }: PropsWithChildren & { modal: ReactNode }) => {
const session = await auth();
return (
<SessionProvider session={session}>
<div className=" tw-app-layout">
<Header />
<Suspense fallback={<PageSkeleton />}>{children}</Suspense>
@ -22,6 +27,7 @@ const appLayout: React.FC<PropsWithChildren & { modal: ReactNode }> = ({ childre
<ToastProvider>
<Toast />
</ToastProvider>
</>
);
export default appLayout;
</SessionProvider>
);
};
export default AppLayout;

View File

@ -17,6 +17,7 @@ const App: React.FC<{ searchParams: Record<string, any> }> = 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('/');
}

View File

@ -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 (
<>

View File

@ -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<string | undefined>('');
const [success, setSuccess] = useState<string | undefined>('');
const [isPending, startTransition] = useTransition();
const searchParams = useSearchParams();
const urlError =
searchParams.get('error') === 'OAuthAccountNotLinked'
? 'Email already in use with different Provider!'
: '';
const form = useForm<z.infer<typeof LoginSchema>>({
mode: 'all',
defaultValues: {
@ -20,6 +39,27 @@ const LoginForm: FC = () => {
},
resolver: zodResolver(LoginSchema),
});
const onSubmit = (data: z.infer<typeof LoginSchema>) => {
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 (
<CardWrapper
headerLabel="欢迎登录"
@ -28,10 +68,73 @@ const LoginForm: FC = () => {
showSocial
>
<Form {...form}>
<form>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div>
<FormField />
{showTwoFactor && (
<>
<FormField
control={form.control}
name="code"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
type="text"
placeholder="请输入验证码"
/>
</FormControl>
</FormItem>
)}
/>
</>
)}
{!showTwoFactor && (
<>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
type="email"
placeholder="请输入邮箱"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
{...field}
type="password"
placeholder="请输入密码"
disabled={isPending}
/>
</FormControl>
</FormItem>
)}
/>
</>
)}
</div>
<ErrorMessage message={error || urlError} />
<SuccessMessage message={success} />
<Button disabled={isPending} type="submit" className="w-full">
{showTwoFactor ? 'Confirm' : 'Login'}
</Button>
</form>
</Form>
</CardWrapper>

View File

@ -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()),
});

View File

@ -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<PostCreateFormRef, PostActionFormProps>((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 || '');

View File

@ -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,防止在详情页后退后返回到创建或编辑页面的弹出框

View File

@ -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<typeof LoginSchema>, 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;
};

View File

@ -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<TwoFactorToken> => {
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 } });
};

View File

@ -1,3 +1 @@
import { handlers } from '@/auth';
export const { GET, POST } = handlers;
export { GET, POST } from '@/auth';

View File

@ -28,3 +28,11 @@ export const sendPasswordResetEmail = async (params: Emailparams) => {
html: `<p>Click <a href="${resetLink}">here</a> to reset password.</p>`,
});
};
export const sendTwoFactorTokenEmail = async ({ email, token }: Emailparams) => {
await transporter.sendMail({
from: '450255477@qq.com',
to: email,
subject: 'Two-factor authentication',
html: `<p>Your two-factor authentication code is: ${token}</p>`,
});
};

View File

@ -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'));
});
};

View File

@ -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;
};

View File

@ -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;

View File

@ -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`);

View File

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE `posts` MODIFY `description` TEXT NULL,
MODIFY `slug` VARCHAR(255) NULL;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `posts` ALTER COLUMN `userId` DROP DEFAULT;

View File

@ -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;

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `users` ADD COLUMN `hashpassword` VARCHAR(191) NULL;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "two_factor_tokens" ALTER COLUMN "code" DROP NOT NULL;

View File

@ -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"
provider = "postgresql"

View File

@ -10,6 +10,6 @@ generator client {
}
datasource db {
provider = "mysql"
provider = "postgresql"
url = env("DATABASE_URL")
}

View File

@ -3,6 +3,7 @@ model TwoFactorToken {
email String
token String @unique
expires DateTime
code String?
@@unique([email, token])
@@map("two_factor_tokens")

View File

@ -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);
});

View File

@ -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 = '/';

View File

@ -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:*",