mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Merge pull request #11 from aaronpo97/confirm-user
Implement confirm user functionality
This commit is contained in:
@@ -59,7 +59,7 @@ const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: numb
|
|||||||
<div className="tooltip tooltip-left" data-tip={`Edit '${beerPost.name}'`}>
|
<div className="tooltip tooltip-left" data-tip={`Edit '${beerPost.name}'`}>
|
||||||
<Link
|
<Link
|
||||||
href={`/beers/${beerPost.id}/edit`}
|
href={`/beers/${beerPost.id}/edit`}
|
||||||
className="btn btn-outline btn-sm"
|
className="btn-outline btn-sm btn"
|
||||||
>
|
>
|
||||||
<FaRegEdit className="text-xl" />
|
<FaRegEdit className="text-xl" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const Navbar = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-none lg:hidden">
|
<div className="flex-none lg:hidden">
|
||||||
<div className="dropdown-end dropdown">
|
<div className="dropdown dropdown-end">
|
||||||
<label tabIndex={0} className="btn btn-ghost btn-circle">
|
<label tabIndex={0} className="btn btn-ghost btn-circle">
|
||||||
<span className="w-10 rounded-full">
|
<span className="w-10 rounded-full">
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const Button: FunctionComponent<FormButtonProps> = ({
|
|||||||
// eslint-disable-next-line react/button-has-type
|
// eslint-disable-next-line react/button-has-type
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
className={`btn-primary btn w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
className={`btn btn-primary w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -9,24 +9,24 @@ import { z } from 'zod';
|
|||||||
import { MAX_AGE, setTokenCookie, getTokenCookie } from './cookie';
|
import { MAX_AGE, setTokenCookie, getTokenCookie } from './cookie';
|
||||||
import ServerError from '../util/ServerError';
|
import ServerError from '../util/ServerError';
|
||||||
|
|
||||||
const { TOKEN_SECRET } = process.env;
|
const { SESSION_SECRET } = process.env;
|
||||||
|
|
||||||
export async function setLoginSession(
|
export async function setLoginSession(
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
session: z.infer<typeof BasicUserInfoSchema>,
|
session: z.infer<typeof BasicUserInfoSchema>,
|
||||||
) {
|
) {
|
||||||
if (!TOKEN_SECRET) {
|
if (!SESSION_SECRET) {
|
||||||
throw new ServerError('Authentication is not configured.', 500);
|
throw new ServerError('Authentication is not configured.', 500);
|
||||||
}
|
}
|
||||||
const createdAt = Date.now();
|
const createdAt = Date.now();
|
||||||
const obj = { ...session, createdAt, maxAge: MAX_AGE };
|
const obj = { ...session, createdAt, maxAge: MAX_AGE };
|
||||||
const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults);
|
const token = await Iron.seal(obj, SESSION_SECRET, Iron.defaults);
|
||||||
|
|
||||||
setTokenCookie(res, token);
|
setTokenCookie(res, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLoginSession(req: SessionRequest) {
|
export async function getLoginSession(req: SessionRequest) {
|
||||||
if (!TOKEN_SECRET) {
|
if (!SESSION_SECRET) {
|
||||||
throw new ServerError('Authentication is not configured.', 500);
|
throw new ServerError('Authentication is not configured.', 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export async function getLoginSession(req: SessionRequest) {
|
|||||||
throw new ServerError('You are not logged in.', 401);
|
throw new ServerError('You are not logged in.', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults);
|
const session = await Iron.unseal(token, SESSION_SECRET, Iron.defaults);
|
||||||
|
|
||||||
const parsed = UserSessionSchema.safeParse(session);
|
const parsed = UserSessionSchema.safeParse(session);
|
||||||
|
|
||||||
|
|||||||
28
config/jwt/index.ts
Normal file
28
config/jwt/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { BasicUserInfoSchema } from '@/config/auth/types';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const { CONFIRMATION_TOKEN_SECRET } = process.env;
|
||||||
|
|
||||||
|
if (!CONFIRMATION_TOKEN_SECRET) {
|
||||||
|
throw new Error('CONFIRMATION_TOKEN_SECRET is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
type User = z.infer<typeof BasicUserInfoSchema>;
|
||||||
|
|
||||||
|
export const generateConfirmationToken = (user: User) => {
|
||||||
|
const token = jwt.sign(user, CONFIRMATION_TOKEN_SECRET, { expiresIn: '30m' });
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const verifyConfirmationToken = (token: string) => {
|
||||||
|
const decoded = jwt.verify(token, CONFIRMATION_TOKEN_SECRET);
|
||||||
|
|
||||||
|
const parsed = BasicUserInfoSchema.safeParse(decoded);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error('Invalid token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.data;
|
||||||
|
};
|
||||||
@@ -15,7 +15,7 @@ const getCurrentUser = async (
|
|||||||
const user = await findUserById(session?.id);
|
const user = await findUserById(session?.id);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ServerError('Could not get user.', 401);
|
throw new ServerError('User is not logged in.', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = user;
|
req.user = user;
|
||||||
|
|||||||
11
config/sparkpost/client.ts
Normal file
11
config/sparkpost/client.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import SparkPost from 'sparkpost';
|
||||||
|
|
||||||
|
const { SPARKPOST_API_KEY } = process.env;
|
||||||
|
|
||||||
|
if (!SPARKPOST_API_KEY) {
|
||||||
|
throw new Error('SPARKPOST_API_KEY is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new SparkPost(SPARKPOST_API_KEY);
|
||||||
|
|
||||||
|
export default client;
|
||||||
25
config/sparkpost/sendEmail.ts
Normal file
25
config/sparkpost/sendEmail.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import client from './client';
|
||||||
|
|
||||||
|
interface EmailParams {
|
||||||
|
address: string;
|
||||||
|
text: string;
|
||||||
|
html: string;
|
||||||
|
subject: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { SPARKPOST_SENDER_ADDRESS } = process.env;
|
||||||
|
|
||||||
|
if (!SPARKPOST_SENDER_ADDRESS) {
|
||||||
|
throw new Error('SPARKPOST_SENDER_ADDRESS env variable is not set.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendEmail = async ({ address, text, html, subject }: EmailParams) => {
|
||||||
|
const from = SPARKPOST_SENDER_ADDRESS;
|
||||||
|
|
||||||
|
await client.transmissions.send({
|
||||||
|
content: { from, html, subject, text },
|
||||||
|
recipients: [{ address }],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sendEmail;
|
||||||
46
emails/Welcome.tsx
Normal file
46
emails/Welcome.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Container, Heading, Text, Button, Section } from '@react-email/components';
|
||||||
|
import { Tailwind } from '@react-email/tailwind';
|
||||||
|
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
interface WelcomeEmail {
|
||||||
|
subject?: string;
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Welcome: FC<WelcomeEmail> = ({ name, url }) => (
|
||||||
|
<Tailwind>
|
||||||
|
<Container className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<Section>
|
||||||
|
<Heading className="text-2xl font-bold">Welcome to The Biergarten App!</Heading>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
Hi {name}, welcome to The Biergarten App! We are excited to have you as a member
|
||||||
|
of our community.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
The Biergarten App is a social network for beer lovers. Here you can share your
|
||||||
|
favorite beers with the community, and discover new ones. You can also create
|
||||||
|
your own beer list, and share it with your friends.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
To get started, please verify your email address by clicking the button below.
|
||||||
|
Once you do so, you will be able to create your profile and start sharing your
|
||||||
|
favorite beers with the community.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button href={url}>Verify Email</Button>
|
||||||
|
|
||||||
|
<Text className="italic">
|
||||||
|
Please note that this email was automatically generated, and we kindly ask you
|
||||||
|
not to reply to it.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Tailwind>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Welcome;
|
||||||
5176
package-lock.json
generated
5176
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -9,41 +9,49 @@
|
|||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"format": "npx prettier . --write",
|
"format": "npx prettier . --write",
|
||||||
"prestart": "npm run build",
|
"prestart": "npm run build",
|
||||||
"prismaDev": "dotenv -e .env.local prisma migrate dev",
|
|
||||||
"seed": "npx ts-node ./prisma/seed/index.ts"
|
"seed": "npx ts-node ./prisma/seed/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/iron": "^7.0.1",
|
"@hapi/iron": "^7.0.1",
|
||||||
"@hookform/resolvers": "^2.9.10",
|
"@hookform/resolvers": "^2.9.10",
|
||||||
"@prisma/client": "^4.10.1",
|
"@prisma/client": "^4.10.1",
|
||||||
|
"@react-email/components": "^0.0.2",
|
||||||
|
"@react-email/render": "0.0.6",
|
||||||
|
"@react-email/tailwind": "0.0.6",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"cloudinary": "^1.34.0",
|
"cloudinary": "^1.34.0",
|
||||||
"cookie": "0.5.0",
|
"cookie": "0.5.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"multer": "^2.0.0-rc.4",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"multer-storage-cloudinary": "^4.0.0",
|
"multer-storage-cloudinary": "^4.0.0",
|
||||||
"next": "^13.2.1",
|
"multer": "^2.0.0-rc.4",
|
||||||
"next-connect": "^1.0.0-next.3",
|
"next-connect": "^1.0.0-next.3",
|
||||||
"passport": "^0.6.0",
|
"next": "^13.2.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"pino": "^8.11.0",
|
"passport": "^0.6.0",
|
||||||
"pino-pretty": "^9.3.0",
|
"pino-pretty": "^9.3.0",
|
||||||
"react": "18.2.0",
|
"pino": "^8.11.0",
|
||||||
"react-daisyui": "^3.0.3",
|
"react-daisyui": "^3.0.3",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-email": "^1.7.15",
|
||||||
"react-hook-form": "^7.43.2",
|
"react-hook-form": "^7.43.2",
|
||||||
"react-icons": "^4.7.1",
|
"react-icons": "^4.7.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"sparkpost": "^2.1.4",
|
||||||
"swr": "^2.0.3",
|
"swr": "^2.0.3",
|
||||||
"zod": "^3.20.6"
|
"zod": "^3.20.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^7.6.0",
|
"@faker-js/faker": "^7.6.0",
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "^0.5.1",
|
||||||
|
"@types/ejs": "^3.1.2",
|
||||||
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^18.14.1",
|
"@types/node": "^18.14.1",
|
||||||
"@types/passport-local": "^1.0.35",
|
"@types/passport-local": "^1.0.35",
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@types/sparkpost": "^2.1.5",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"daisyui": "^2.51.0",
|
"daisyui": "^2.51.0",
|
||||||
"dotenv-cli": "^7.0.0",
|
"dotenv-cli": "^7.0.0",
|
||||||
|
|||||||
56
pages/api/users/confirm.ts
Normal file
56
pages/api/users/confirm.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import { verifyConfirmationToken } from '@/config/jwt';
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import ServerError from '@/config/util/ServerError';
|
||||||
|
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import updateUserToBeConfirmedById from '@/services/User/updateUserToBeConfirmedById';
|
||||||
|
|
||||||
|
const ConfirmUserValidationSchema = z.object({ token: z.string() });
|
||||||
|
|
||||||
|
interface ConfirmUserRequest extends UserExtendedNextApiRequest {
|
||||||
|
query: z.infer<typeof ConfirmUserValidationSchema>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmUser = async (req: ConfirmUserRequest, res: NextApiResponse) => {
|
||||||
|
const { token } = req.query;
|
||||||
|
|
||||||
|
const user = req.user!;
|
||||||
|
const { id } = verifyConfirmationToken(token);
|
||||||
|
|
||||||
|
if (user.id !== id) {
|
||||||
|
throw new ServerError('Could not confirm user.', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isAccountVerified) {
|
||||||
|
throw new ServerError('User is already verified.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateUserToBeConfirmedById(id);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'User confirmed successfully.',
|
||||||
|
statusCode: 200,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
ConfirmUserRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
getCurrentUser,
|
||||||
|
validateRequest({ querySchema: ConfirmUserValidationSchema }),
|
||||||
|
confirmUser,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
|
||||||
|
export default handler;
|
||||||
@@ -11,10 +11,18 @@ import findUserByEmail from '@/services/User/findUserByEmail';
|
|||||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
|
||||||
|
import sendConfirmationEmail from '@/services/User/sendConfirmationEmail';
|
||||||
|
|
||||||
interface RegisterUserRequest extends NextApiRequest {
|
interface RegisterUserRequest extends NextApiRequest {
|
||||||
body: z.infer<typeof CreateUserValidationSchema>;
|
body: z.infer<typeof CreateUserValidationSchema>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { BASE_URL } = process.env;
|
||||||
|
|
||||||
|
if (!BASE_URL) {
|
||||||
|
throw new ServerError('BASE_URL env variable is not set.', 500);
|
||||||
|
}
|
||||||
|
|
||||||
const registerUser = async (req: RegisterUserRequest, res: NextApiResponse) => {
|
const registerUser = async (req: RegisterUserRequest, res: NextApiResponse) => {
|
||||||
const [usernameTaken, emailTaken] = await Promise.all([
|
const [usernameTaken, emailTaken] = await Promise.all([
|
||||||
findUserByUsername(req.body.username),
|
findUserByUsername(req.body.username),
|
||||||
@@ -41,11 +49,14 @@ const registerUser = async (req: RegisterUserRequest, res: NextApiResponse) => {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
});
|
});
|
||||||
res.status(201).json({
|
|
||||||
message: 'User created successfully.',
|
await sendConfirmationEmail(user);
|
||||||
payload: user,
|
|
||||||
statusCode: 201,
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
statusCode: 200,
|
||||||
|
message: 'User registered successfully.',
|
||||||
|
payload: user,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
2
prisma/migrations/20230304185749_/migration.sql
Normal file
2
prisma/migrations/20230304185749_/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "isAccountVerified" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -19,6 +19,7 @@ model User {
|
|||||||
email String @unique
|
email String @unique
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||||
|
isAccountVerified Boolean @default(false)
|
||||||
dateOfBirth DateTime
|
dateOfBirth DateTime
|
||||||
beerPosts BeerPost[]
|
beerPosts BeerPost[]
|
||||||
beerTypes BeerType[]
|
beerTypes BeerType[]
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const createNewUser = async ({
|
|||||||
lastName: true,
|
lastName: true,
|
||||||
dateOfBirth: true,
|
dateOfBirth: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
isAccountVerified: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const findUserById = async (id: string) => {
|
|||||||
lastName: true,
|
lastName: true,
|
||||||
dateOfBirth: true,
|
dateOfBirth: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
isAccountVerified: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const GetUserSchema = z.object({
|
|||||||
firstName: z.string(),
|
firstName: z.string(),
|
||||||
lastName: z.string(),
|
lastName: z.string(),
|
||||||
dateOfBirth: z.coerce.date(),
|
dateOfBirth: z.coerce.date(),
|
||||||
|
isAccountVerified: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default GetUserSchema;
|
export default GetUserSchema;
|
||||||
|
|||||||
32
services/User/sendConfirmationEmail.ts
Normal file
32
services/User/sendConfirmationEmail.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { generateConfirmationToken } from '@/config/jwt';
|
||||||
|
import sendEmail from '@/config/sparkpost/sendEmail';
|
||||||
|
|
||||||
|
import ServerError from '@/config/util/ServerError';
|
||||||
|
import Welcome from '@/emails/Welcome';
|
||||||
|
import { render } from '@react-email/render';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import GetUserSchema from './schema/GetUserSchema';
|
||||||
|
|
||||||
|
const { BASE_URL } = process.env;
|
||||||
|
|
||||||
|
if (!BASE_URL) {
|
||||||
|
throw new ServerError('BASE_URL env variable is not set.', 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSchema = z.infer<typeof GetUserSchema>;
|
||||||
|
|
||||||
|
const sendConfirmationEmail = async ({ id, username, email }: UserSchema) => {
|
||||||
|
const confirmationToken = generateConfirmationToken({ id, username });
|
||||||
|
|
||||||
|
const subject = 'Confirm your email';
|
||||||
|
const name = username;
|
||||||
|
const url = `${BASE_URL}/api/users/confirm?token=${confirmationToken}`;
|
||||||
|
const address = email;
|
||||||
|
|
||||||
|
const html = render(Welcome({ name, url, subject })!);
|
||||||
|
const text = render(Welcome({ name, url, subject })!, { plainText: true });
|
||||||
|
|
||||||
|
await sendEmail({ address, subject, text, html });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sendConfirmationEmail;
|
||||||
24
services/User/updateUserToBeConfirmedById.ts
Normal file
24
services/User/updateUserToBeConfirmedById.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const updateUserToBeConfirmedById = async (id: string) => {
|
||||||
|
const user: z.infer<typeof GetUserSchema> = await DBClient.instance.user.update({
|
||||||
|
where: { id },
|
||||||
|
data: { isAccountVerified: true, updatedAt: new Date() },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
isAccountVerified: true,
|
||||||
|
createdAt: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
dateOfBirth: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateUserToBeConfirmedById;
|
||||||
Reference in New Issue
Block a user