Compare commits
7 Commits
feat/admin
...
feat/new-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27f6c9b8eb | ||
|
|
0456fe9826 | ||
|
|
2798cd624d | ||
|
|
02569619f9 | ||
|
|
590344f793 | ||
|
|
bc11abda08 | ||
|
|
aa740864b8 |
@@ -17,3 +17,10 @@ NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED=false
|
||||
# This is only required for the marketing site
|
||||
NEXT_PRIVATE_REDIS_URL=
|
||||
NEXT_PRIVATE_REDIS_TOKEN=
|
||||
|
||||
# Mailserver
|
||||
NEXT_PRIVATE_SENDGRID_API_KEY=
|
||||
NEXT_PRIVATE_SMTP_MAIL_HOST=
|
||||
NEXT_PRIVATE_SMTP_MAIL_PORT=
|
||||
NEXT_PRIVATE_SMTP_MAIL_USER=
|
||||
NEXT_PRIVATE_SMTP_MAIL_PASSWORD=
|
||||
@@ -43,9 +43,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<PlausibleProvider>
|
||||
{children}
|
||||
</PlausibleProvider>
|
||||
<PlausibleProvider>{children}</PlausibleProvider>
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
"next-auth": "^4.22.1",
|
||||
"next-plausible": "^3.7.2",
|
||||
"next-themes": "^0.2.1",
|
||||
"nodemailer": "^6.9.3",
|
||||
"nodemailer-sendgrid": "^1.0.3",
|
||||
"perfect-freehand": "^1.2.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
@@ -38,6 +40,8 @@
|
||||
"devDependencies": {
|
||||
"@types/formidable": "^2.0.6",
|
||||
"@types/node": "20.1.0",
|
||||
"@types/nodemailer": "^6.4.8",
|
||||
"@types/nodemailer-sendgrid": "^1.0.0",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4"
|
||||
}
|
||||
|
||||
6
apps/web/process-env.d.ts
vendored
6
apps/web/process-env.d.ts
vendored
@@ -11,5 +11,11 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||
|
||||
NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED: string;
|
||||
|
||||
NEXT_PRIVATE_SENDGRID_API_KEY: string;
|
||||
NEXT_PRIVATE_SMTP_MAIL_HOST: string;
|
||||
NEXT_PRIVATE_SMTP_MAIL_PORT: string;
|
||||
NEXT_PRIVATE_SMTP_MAIL_USER: string;
|
||||
NEXT_PRIVATE_SMTP_MAIL_PASSWORD: string;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
apps/web/public/static/clock.png
Normal file
BIN
apps/web/public/static/clock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 541 B |
BIN
apps/web/public/static/completed.png
Normal file
BIN
apps/web/public/static/completed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 553 B |
BIN
apps/web/public/static/document.png
Normal file
BIN
apps/web/public/static/document.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/web/public/static/download.png
Normal file
BIN
apps/web/public/static/download.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 346 B |
BIN
apps/web/public/static/logo.png
Normal file
BIN
apps/web/public/static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
BIN
apps/web/public/static/review.png
Normal file
BIN
apps/web/public/static/review.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 332 B |
132
apps/web/src/app/send/page.tsx
Normal file
132
apps/web/src/app/send/page.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { TSendMailMutationSchema } from '@documenso/trpc/server/mail-router/schema';
|
||||
|
||||
export default function Send() {
|
||||
const { mutateAsync: sendMail } = trpc.mail.send.useMutation();
|
||||
const [form, setForm] = useState<TSendMailMutationSchema>({
|
||||
email: '',
|
||||
type: 'invite',
|
||||
documentName: '',
|
||||
name: '',
|
||||
firstName: '',
|
||||
documentSigningLink: '',
|
||||
downloadLink: '',
|
||||
numberOfSigners: 1,
|
||||
reviewLink: '',
|
||||
});
|
||||
|
||||
const handleInputChange = (event: { target: { name: any; value: unknown } }) => {
|
||||
setForm({
|
||||
...form,
|
||||
[event.target.name]: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: { preventDefault: () => void }) => {
|
||||
event.preventDefault();
|
||||
|
||||
console.log('clicked');
|
||||
|
||||
await sendMail(form);
|
||||
|
||||
alert('sent');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-20">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
placeholder="Email"
|
||||
value={form.email}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="my-2 block rounded-md border-2 border-gray-300 p-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="type"
|
||||
placeholder="Type"
|
||||
value={form.type}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="my-2 block rounded-md border-2 border-gray-300 p-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="documentName"
|
||||
placeholder="Document Name"
|
||||
value={form.documentName}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="my-2 block rounded-md border-2 border-gray-300 p-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
value={form.name}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="my-2 block rounded-md border-2 border-gray-300 p-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
placeholder="First Name"
|
||||
value={form.firstName}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="my-2 block rounded-md border-2 border-gray-300 p-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="documentSigningLink"
|
||||
placeholder="Document Signing Link"
|
||||
value={form.documentSigningLink}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="my-2 block rounded-md border-2 border-gray-300 p-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="downloadLink"
|
||||
placeholder="Download Link"
|
||||
value={form.downloadLink}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="my-2 block rounded-md border-2 border-gray-300 p-2"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
name="numberOfSigners"
|
||||
placeholder="Number of Signers"
|
||||
value={form.numberOfSigners}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="my-2 block rounded-md border-2 border-gray-300 p-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="reviewLink"
|
||||
placeholder="Review Link"
|
||||
value={form.reviewLink}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="my-2 block rounded-md border-2 border-gray-300 p-2"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-4 rounded-md border-2 border-solid border-black px-4 py-2 text-2xl"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3160
package-lock.json
generated
3160
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -5,13 +5,17 @@
|
||||
"dev": "turbo run dev --filter=@documenso/{web,marketing}",
|
||||
"start": "cd apps && cd web && next start",
|
||||
"lint": "turbo run lint",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"docker:compose-up": "docker-compose -p documenso -f ./docker/compose-without-app.yml up -d",
|
||||
"dx": "npm install && run-s docker:compose-up db:migrate && npm run dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "^16.0.3",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-custom": "*",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.5.1",
|
||||
"turbo": "^1.9.3"
|
||||
},
|
||||
@@ -20,5 +24,8 @@
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
]
|
||||
],
|
||||
"prisma": {
|
||||
"schema": "packages/prisma/schema.prisma"
|
||||
}
|
||||
}
|
||||
|
||||
199
packages/lib/mail/template.tsx
Normal file
199
packages/lib/mail/template.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
render,
|
||||
} from '@react-email/components';
|
||||
|
||||
interface DocumensoEmailProps {
|
||||
email?: string;
|
||||
name?: string;
|
||||
firstName?: string;
|
||||
documentSigningLink?: string;
|
||||
documentName?: string;
|
||||
downloadLink?: string;
|
||||
reviewLink?: string;
|
||||
numberOfSigners?: number;
|
||||
type: 'invite' | 'signed' | 'completed';
|
||||
}
|
||||
|
||||
export const DocumensoEmail = ({
|
||||
documentSigningLink = 'https://documenso.com',
|
||||
downloadLink = 'https://documenso.com',
|
||||
reviewLink = 'https://documenso.com',
|
||||
email = 'duncan@documenso.com',
|
||||
name = 'Ephraim Atta-Duncan',
|
||||
firstName = 'Ephraim',
|
||||
documentName = 'Open Source Pledge.pdf',
|
||||
numberOfSigners = 2,
|
||||
type = 'signed',
|
||||
}: DocumensoEmailProps) => {
|
||||
const previewText = type === 'completed' ? 'Completed Document' : `Sign Document`;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind>
|
||||
<Body className="mx-auto my-auto ml-auto mr-auto font-sans">
|
||||
<Section className="bg-white">
|
||||
<Container
|
||||
style={{
|
||||
border: '2px solid #eaeaea',
|
||||
}}
|
||||
className="mx-auto mb-[10px] ml-auto mr-auto mt-[40px] w-[600px] rounded-lg p-[10px] backdrop-blur-sm"
|
||||
>
|
||||
<Section>
|
||||
<Img
|
||||
src={`http://localhost:3000/static/logo.png`}
|
||||
alt="Documenso Logo"
|
||||
width={120}
|
||||
/>
|
||||
|
||||
<Section className="mt-4 flex-row items-center justify-center">
|
||||
<div className="my-3 flex items-center justify-center">
|
||||
<Img
|
||||
className="ml-[160px]" // Works on most of the email clients
|
||||
src={`http://localhost:3000/static/document.png`}
|
||||
alt="Documenso"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{type === 'completed' && (
|
||||
<Text className="mb-4 text-center text-[16px] font-semibold text-[#7AC455]">
|
||||
<Img
|
||||
src="http://localhost:3000/static/completed.png"
|
||||
className="-mb-0.5 mr-1.5 inline"
|
||||
/>
|
||||
Completed
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{type === 'signed' && (
|
||||
<Text className="mb-4 text-center text-[16px] font-semibold text-[#3879C5]">
|
||||
<Img
|
||||
src="http://localhost:3000/static/clock.png"
|
||||
className="-mb-0.5 mr-1.5 inline"
|
||||
/>
|
||||
Waiting for {numberOfSigners} {numberOfSigners === 1 ? 'person' : 'people'} to
|
||||
sign
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text className="mx-0 mb-0 text-center text-[16px] font-semibold text-[#27272A]">
|
||||
{type === 'invite'
|
||||
? `${name} has invited you to sign “${documentName}”`
|
||||
: `“${documentName}” was signed by ${name}`}
|
||||
</Text>
|
||||
<Text className="my-1 text-center text-[14px] text-[#AFAFAF]">
|
||||
{type === 'invite'
|
||||
? 'Continue by signing the document.'
|
||||
: 'Continue by downloading or reviewing the document.'}
|
||||
</Text>
|
||||
<Section className="mb-[24px] mt-[32px] text-center">
|
||||
{type === 'invite' && (
|
||||
<Button
|
||||
pX={20}
|
||||
pY={12}
|
||||
className="rounded bg-[#A2E771] text-center text-[14px] font-medium text-black no-underline"
|
||||
href={documentSigningLink}
|
||||
>
|
||||
Sign Document
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{type !== 'invite' && (
|
||||
<Section>
|
||||
<Button
|
||||
pX={18}
|
||||
pY={10}
|
||||
style={{
|
||||
border: '1px solid #E9E9E9',
|
||||
}}
|
||||
className="mr-4 rounded-lg text-center text-[14px] font-medium text-black no-underline"
|
||||
href={reviewLink}
|
||||
>
|
||||
<Img
|
||||
src="http://localhost:3000/static/review.png"
|
||||
className="-mb-0.5 mr-1 inline"
|
||||
/>
|
||||
Review
|
||||
</Button>
|
||||
<Button
|
||||
pX={18}
|
||||
pY={10}
|
||||
style={{
|
||||
border: '1px solid #E9E9E9',
|
||||
}}
|
||||
className="rounded-lg text-center text-[14px] font-medium text-black no-underline"
|
||||
href={downloadLink}
|
||||
>
|
||||
<Img
|
||||
src="http://localhost:3000/static/download.png"
|
||||
className="-mb-0.5 mr-1 inline"
|
||||
/>
|
||||
Download
|
||||
</Button>
|
||||
</Section>
|
||||
)}
|
||||
</Section>
|
||||
</Section>
|
||||
</Section>
|
||||
</Container>
|
||||
<Container className="mx-auto ml-auto mr-auto w-[600px]">
|
||||
<Section>
|
||||
{type === 'invite' && (
|
||||
<>
|
||||
<Text className="text-[18px] leading-[24px] text-black">
|
||||
{name} <span className="font-semibold text-[#AFAFAF]">({email})</span>
|
||||
</Text>
|
||||
<Text className="mb-[40px] text-[16px] leading-[28px] text-[#AFAFAF]">
|
||||
Hi,
|
||||
<br />
|
||||
Please sign the attached document. Magna magna adipisicing dolore minim et
|
||||
aliquip ipsum esse ut nulla ad sint irure.
|
||||
<br /> - {firstName}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text className="my-[40px] text-[14px] text-[#AFAFAF]">
|
||||
This document was sent using{' '}
|
||||
<Link className="text-[#7AC455] underline" href="https://documenso.com">
|
||||
Documenso.
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Text className="my-[40px] text-[14px] text-[#AFAFAF]">
|
||||
Documenso
|
||||
<br />
|
||||
2261 Market Street, #5211, San Francisco, CA 94114, USA
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
</Section>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export const emailHtml = (props: DocumensoEmailProps) =>
|
||||
render(<DocumensoEmail {...props} />, {
|
||||
pretty: true,
|
||||
});
|
||||
|
||||
export const emailText = (props: DocumensoEmailProps) =>
|
||||
render(<DocumensoEmail {...props} />, {
|
||||
plainText: true,
|
||||
});
|
||||
@@ -10,17 +10,18 @@
|
||||
"universal/",
|
||||
"next-auth/"
|
||||
],
|
||||
"scripts": {
|
||||
},
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"@documenso/prisma": "*",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@next-auth/prisma-adapter": "^1.0.6",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@react-email/components": "^0.0.7",
|
||||
"@react-email/render": "^0.0.7",
|
||||
"@upstash/redis": "^1.20.6",
|
||||
"bcrypt": "^5.1.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"next": "13.4.1",
|
||||
"next-auth": "^4.22.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"stripe": "^12.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
51
packages/lib/server-only/mail/send.ts
Normal file
51
packages/lib/server-only/mail/send.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import nodemailerSendgrid from 'nodemailer-sendgrid';
|
||||
|
||||
import { TSendMailMutationSchema } from '@documenso/trpc/server/mail-router/schema';
|
||||
|
||||
import { emailHtml, emailText } from '../../mail/template';
|
||||
|
||||
interface SendMail {
|
||||
template: TSendMailMutationSchema;
|
||||
mail: {
|
||||
from: string;
|
||||
subject: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const sendMail = async ({ template, mail }: SendMail) => {
|
||||
let transporter;
|
||||
|
||||
if (process.env.NEXT_PRIVATE_SENDGRID_API_KEY) {
|
||||
transporter = nodemailer.createTransport(
|
||||
nodemailerSendgrid({
|
||||
apiKey: process.env.NEXT_PRIVATE_SENDGRID_API_KEY,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NEXT_PRIVATE_SMTP_MAIL_HOST) {
|
||||
transporter = nodemailer.createTransport({
|
||||
host: process.env.NEXT_PRIVATE_SMTP_MAIL_HOST,
|
||||
port: Number(process.env.NEXT_PRIVATE_SMTP_MAIL_PORT),
|
||||
auth: {
|
||||
user: process.env.NEXT_PRIVATE_SMTP_MAIL_USER,
|
||||
pass: process.env.NEXT_PRIVATE_SMTP_MAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!transporter) {
|
||||
throw new Error(
|
||||
'No mail transport configured. Probably Sendgrid API Key nor SMTP Mail host was set',
|
||||
);
|
||||
}
|
||||
|
||||
await transporter.sendMail({
|
||||
from: mail.from,
|
||||
to: template.email,
|
||||
subject: mail.subject,
|
||||
text: emailText({ ...template }),
|
||||
html: emailHtml({ ...template }),
|
||||
});
|
||||
};
|
||||
27
packages/trpc/server/mail-router/router.ts
Normal file
27
packages/trpc/server/mail-router/router.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { sendMail } from '@documenso/lib/server-only/mail/send';
|
||||
|
||||
import { authenticatedProcedure, router } from '../trpc';
|
||||
import { ZSendMailMutationSchema } from './schema';
|
||||
|
||||
export const mailRouter = router({
|
||||
send: authenticatedProcedure.input(ZSendMailMutationSchema).mutation(async ({ input }) => {
|
||||
try {
|
||||
return await sendMail({
|
||||
template: input,
|
||||
mail: {
|
||||
from: '<hi@documenso>',
|
||||
subject: 'Documeso Invite',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to send an email.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
15
packages/trpc/server/mail-router/schema.ts
Normal file
15
packages/trpc/server/mail-router/schema.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZSendMailMutationSchema = z.object({
|
||||
email: z.string().min(1).email(),
|
||||
name: z.string().min(1).optional(),
|
||||
firstName: z.string().min(1).optional(),
|
||||
documentSigningLink: z.string().min(1).optional(),
|
||||
documentName: z.string().min(1).optional(),
|
||||
downloadLink: z.string().min(1).optional(),
|
||||
reviewLink: z.string().min(1).optional(),
|
||||
numberOfSigners: z.number().int().min(1).optional(),
|
||||
type: z.enum(['invite', 'signed', 'completed']),
|
||||
});
|
||||
|
||||
export type TSendMailMutationSchema = z.infer<typeof ZSendMailMutationSchema>;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { authRouter } from './auth-router/router';
|
||||
import { mailRouter } from './mail-router/router';
|
||||
import { profileRouter } from './profile-router/router';
|
||||
import { procedure, router } from './trpc';
|
||||
|
||||
@@ -6,6 +7,7 @@ export const appRouter = router({
|
||||
hello: procedure.query(() => 'Hello, world!'),
|
||||
auth: authRouter,
|
||||
profile: profileRouter,
|
||||
mail: mailRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
"NEXT_PUBLIC_SITE_URL",
|
||||
"NEXT_PRIVATE_DATABASE_URL",
|
||||
"NEXT_PRIVATE_NEXT_AUTH_SECRET",
|
||||
"NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED"
|
||||
"NEXT_PUBLIC_SUBSCRIPTIONS_ENABLED",
|
||||
|
||||
"NEXT_PRIVATE_SENDGRID_API_KEY",
|
||||
"NEXT_PRIVATE_SMTP_MAIL_HOST",
|
||||
"NEXT_PRIVATE_SMTP_MAIL_PORT",
|
||||
"NEXT_PRIVATE_SMTP_MAIL_USER",
|
||||
"NEXT_PRIVATE_SMTP_MAIL_PASSWORD"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user