Compare commits
2 Commits
cf0579023e
...
a1cba242e9
Author | SHA1 | Date |
---|---|---|
Suyono | a1cba242e9 | |
Suyono | a607a4528e |
|
@ -5,6 +5,8 @@
|
|||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
/.local/
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
|
@ -25,7 +27,7 @@ yarn-debug.log*
|
|||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*
|
||||
.env.development
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
nodejs 20.8.1
|
||||
pnpm 8.9.2
|
|
@ -0,0 +1,109 @@
|
|||
import {ReactElement} from "react";
|
||||
import {domToReact, Element} from "html-react-parser";
|
||||
import {nunito_sans, raleway} from "@/app/fonts";
|
||||
import {Code} from "bright";
|
||||
import {mark} from "@/bright-extension/mark";
|
||||
|
||||
export type nodeHandlerResult = {match :boolean, element? :ReactElement}
|
||||
export type nodeHandler = (node :Element) => nodeHandlerResult
|
||||
|
||||
function h1(node: Element): nodeHandlerResult {
|
||||
if (node.name === "h1") {
|
||||
if (node.attribs.class === "title") {
|
||||
return {
|
||||
match: true,
|
||||
element: (
|
||||
<h1 className={`${raleway.className} mx-auto w-224 text-4xl`}>
|
||||
{domToReact(node.children)}
|
||||
</h1>
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
match: true,
|
||||
element: (
|
||||
<h1 className={`${raleway.className} mx-auto w-224 text-3xl`}>
|
||||
{domToReact(node.children)}
|
||||
</h1>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return { match: false};
|
||||
}
|
||||
|
||||
function h2(node: Element): nodeHandlerResult {
|
||||
if (node.name === "h2") {
|
||||
return {
|
||||
match: true,
|
||||
element: (
|
||||
<h1 className={`${raleway.className} mx-auto w-224 text-2xl`}>
|
||||
{domToReact(node.children)}
|
||||
</h1>
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
return {match: false};
|
||||
}
|
||||
|
||||
function h3(node: Element): nodeHandlerResult {
|
||||
if (node.name === "h3") {
|
||||
return {
|
||||
match: true,
|
||||
element: (
|
||||
<h1 className={`${raleway.className} mx-auto w-224 text-xl`}>
|
||||
{domToReact(node.children)}
|
||||
</h1>
|
||||
)
|
||||
};
|
||||
}
|
||||
return {match: false};
|
||||
}
|
||||
|
||||
function code(node: Element): nodeHandlerResult {
|
||||
if (node.name === "code") {
|
||||
let linenumber = false;
|
||||
if ("class" in node.attribs) {
|
||||
const classes = node.attribs.class.split(" ");
|
||||
if (classes.includes("linenumber")) {
|
||||
linenumber = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
match: true,
|
||||
element: (
|
||||
<div className={`w-224 mx-auto`}>
|
||||
<Code
|
||||
lang={`${node.attribs.lang}`}
|
||||
lineNumbers={linenumber}
|
||||
extensions={[mark]}
|
||||
>
|
||||
{domToReact(node.children)}
|
||||
</Code>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
return {match: false};
|
||||
}
|
||||
|
||||
function p(node: Element): nodeHandlerResult {
|
||||
if (node.name === "p") {
|
||||
if (node.attribs.class === "paragraph") {
|
||||
return {
|
||||
match: true,
|
||||
element: (
|
||||
<h1 className={`${nunito_sans.className} mx-auto w-224`}>
|
||||
{domToReact(node.children)}
|
||||
</h1>
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
return {match: false};
|
||||
}
|
||||
|
||||
export const handlers: nodeHandler[] = [h1, h2, h3, code, p];
|
|
@ -1,62 +1,21 @@
|
|||
import { getPost } from "@/backend/post";
|
||||
import DOMPurify from "dompurify";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { nunito_sans, raleway } from "@/app/fonts";
|
||||
import parse, {
|
||||
domToReact,
|
||||
Element,
|
||||
// domToReact,
|
||||
HTMLReactParserOptions,
|
||||
} from "html-react-parser";
|
||||
import { DummyPostSlug, DummyPostString } from "@/components/dummyPost";
|
||||
import { handlers } from "@/app/post/[slug]/nodeHandler";
|
||||
|
||||
const options: HTMLReactParserOptions = {
|
||||
replace: (domNode) => {
|
||||
if (domNode instanceof Element && domNode.attribs) {
|
||||
// console.log(domNode.attribs)
|
||||
if (domNode.name === "h1") {
|
||||
if (domNode.attribs.class === "title") {
|
||||
return (
|
||||
<h1 className={`${raleway.className} mx-auto w-224 text-4xl`}>
|
||||
{domToReact(domNode.children)}
|
||||
</h1>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<h1 className={`${raleway.className} mx-auto w-224 text-3xl`}>
|
||||
{domToReact(domNode.children)}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
} else if (domNode.name === "h2") {
|
||||
return (
|
||||
<h1 className={`${raleway.className} mx-auto w-224 text-2xl`}>
|
||||
{domToReact(domNode.children)}
|
||||
</h1>
|
||||
);
|
||||
} else if (domNode.name === "h3") {
|
||||
return (
|
||||
<h1 className={`${raleway.className} mx-auto w-224 text-xl`}>
|
||||
{domToReact(domNode.children)}
|
||||
</h1>
|
||||
);
|
||||
} else if (domNode.name === "p") {
|
||||
if (domNode.attribs.class === "paragraph") {
|
||||
return (
|
||||
<h1 className={`${nunito_sans.className} mx-auto w-224`}>
|
||||
{domToReact(domNode.children)}
|
||||
</h1>
|
||||
);
|
||||
} else {
|
||||
const classes = domNode.attribs.class.split(" ");
|
||||
if (classes.includes("code")) {
|
||||
if (classes.includes("shell")) {
|
||||
|
||||
} else if (classes.includes("go") || classes.includes("golang")) {
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
for (let handler of handlers) {
|
||||
if (domNode instanceof Element && domNode.attribs) {
|
||||
let result = handler(domNode)
|
||||
if (result.match && typeof result.element !== 'undefined') {
|
||||
return result.element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,12 +23,12 @@ const options: HTMLReactParserOptions = {
|
|||
};
|
||||
|
||||
export default async function Post({ params }: { params: { slug: string } }) {
|
||||
let content = "";
|
||||
let content;
|
||||
|
||||
const dummySlug = await DummyPostSlug();
|
||||
if (dummySlug === params.slug) {
|
||||
content = await DummyPostString();
|
||||
console.log(content);
|
||||
// console.log(content);
|
||||
} else {
|
||||
content = await getPost(params.slug);
|
||||
}
|
||||
|
@ -78,5 +37,9 @@ export default async function Post({ params }: { params: { slug: string } }) {
|
|||
// console.log(content)
|
||||
const elem = parse(content, options);
|
||||
|
||||
return <div className={`flex flex-col`}>{elem}</div>;
|
||||
return (
|
||||
<div className={`flex flex-col`}>
|
||||
{elem}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,25 @@
|
|||
import mysql, { PoolOptions } from "mysql2";
|
||||
import * as fs from 'fs';
|
||||
import * as appEnv from "./env";
|
||||
|
||||
if (typeof process.env.MYSQL_SSL_CA === 'undefined') {
|
||||
throw new Error("missing MYSQL_SSL_CA")
|
||||
}
|
||||
|
||||
if (typeof process.env.MYSQL_SSL_KEY === 'undefined') {
|
||||
throw new Error("missing MYSQL_SSL_KEY")
|
||||
}
|
||||
|
||||
if (typeof process.env.MYSQL_SSL_CERT === 'undefined') {
|
||||
throw new Error("missing MYSQL_SSL_CERT")
|
||||
}
|
||||
|
||||
const access: PoolOptions = {
|
||||
host: process.env.MYSQL_HOST,
|
||||
port: 'MYSQL_PORT' in process.env && typeof process.env.MYSQL_PORT === 'string' ? parseInt(process.env.MYSQL_PORT) : 3306,
|
||||
user: process.env.MYSQL_USER,
|
||||
password: process.env.MYSQL_PASSWORD,
|
||||
database: 'MYSQL_DATABASE' in process.env ? process.env.MYSQL_DATABASE : 'blog',
|
||||
host: appEnv.getMysqlHost(),
|
||||
port: appEnv.getMysqlPort(),
|
||||
user: appEnv.getMysqlUser(),
|
||||
password: appEnv.getMysqlPassword(),
|
||||
database: appEnv.getMysqlDatabase(),
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
maxIdle: 10,
|
||||
|
@ -13,6 +27,11 @@ const access: PoolOptions = {
|
|||
queueLimit: 0,
|
||||
enableKeepAlive: true,
|
||||
keepAliveInitialDelay: 0,
|
||||
ssl: {
|
||||
ca: fs.readFileSync(appEnv.getMysqlSslCaFile()),
|
||||
key: fs.readFileSync(appEnv.getMysqlSslKeyFile()),
|
||||
cert: fs.readFileSync(appEnv.getMysqlSslCertFile())
|
||||
}
|
||||
}
|
||||
|
||||
export const pool = mysql.createPool(access)
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
export function getMysqlHost(): string {
|
||||
if (typeof process.env.MYSQL_HOST === 'undefined') {
|
||||
throw new Error("missing env MYSQL_HOST")
|
||||
}
|
||||
|
||||
return process.env.MYSQL_HOST
|
||||
}
|
||||
|
||||
export function getMysqlPort(): number {
|
||||
if (typeof process.env.MYSQL_PORT === 'undefined') {
|
||||
throw new Error("missing env MYSQL_PORT")
|
||||
}
|
||||
|
||||
return parseInt(process.env.MYSQL_PORT)
|
||||
}
|
||||
|
||||
export function getMysqlUser(): string {
|
||||
if (typeof process.env.MYSQL_USER === 'undefined') {
|
||||
throw new Error("missing env MYSQL_USER")
|
||||
}
|
||||
|
||||
return process.env.MYSQL_USER
|
||||
}
|
||||
|
||||
export function getMysqlPassword(): string {
|
||||
if (typeof process.env.MYSQL_PASSWORD === 'undefined') {
|
||||
throw new Error("missing env MYSQL_PASSWORD")
|
||||
}
|
||||
|
||||
return process.env.MYSQL_PASSWORD
|
||||
}
|
||||
|
||||
export function getMysqlDatabase(): string {
|
||||
if (typeof process.env.MYSQL_DATABASE === 'undefined') {
|
||||
throw new Error("missing env MYSQL_DATABASE")
|
||||
}
|
||||
|
||||
return process.env.MYSQL_DATABASE
|
||||
}
|
||||
|
||||
export function getMysqlSslCaFile(): string {
|
||||
if (typeof process.env.MYSQL_SSL_CA === 'undefined') {
|
||||
throw new Error("missing env MYSQL_SSL_CA")
|
||||
}
|
||||
|
||||
return process.env.MYSQL_SSL_CA
|
||||
}
|
||||
|
||||
export function getMysqlSslCertFile(): string {
|
||||
if (typeof process.env.MYSQL_SSL_CERT === 'undefined') {
|
||||
throw new Error("missing env MYSQL_SSL_CERT")
|
||||
}
|
||||
|
||||
return process.env.MYSQL_SSL_CERT
|
||||
}
|
||||
|
||||
export function getMysqlSslKeyFile(): string {
|
||||
if (typeof process.env.MYSQL_SSL_KEY === 'undefined') {
|
||||
throw new Error("missing env MYSQL_SSL_KEY")
|
||||
}
|
||||
|
||||
return process.env.MYSQL_SSL_KEY
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { Extension } from 'bright';
|
||||
|
||||
export const lineNumbers :Extension = {
|
||||
name: "lineNumbers",
|
||||
beforeHighlight: (props, annotations) => {
|
||||
console.log(annotations);
|
||||
if (annotations.length > 0 ) {
|
||||
return { ...props, lineNumbers: true }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { Extension } from "bright";
|
||||
|
||||
export const mark: Extension = {
|
||||
name: "mark",
|
||||
InlineAnnotation: ({ children, query }) => {
|
||||
return (
|
||||
<mark style={{ background: query }}>{children}</mark>
|
||||
)
|
||||
},
|
||||
MultilineAnnotation: ({ children, query }) => {
|
||||
return (
|
||||
<div style={{ background: query }}>{children}</div>
|
||||
)
|
||||
},
|
||||
};
|
|
@ -1,78 +1,13 @@
|
|||
import { promises as fsp } from 'fs'
|
||||
|
||||
export async function DummyPostString() {
|
||||
const ReactDOMServer = (await import('react-dom/server')).default
|
||||
const component = await DummyPost()
|
||||
return ReactDOMServer.renderToStaticMarkup(component)
|
||||
let path = ""
|
||||
if ('DUMMY_HTML_DIR' in process.env && typeof process.env.DUMMY_HTML_DIR === "string") {
|
||||
path = process.env.DUMMY_HTML_DIR + "test1.html";
|
||||
}
|
||||
return await fsp.readFile(path, "utf-8")
|
||||
}
|
||||
|
||||
export async function DummyPostSlug() {
|
||||
return "dummy-post"
|
||||
}
|
||||
|
||||
export async function DummyPost() {
|
||||
return(
|
||||
<div>
|
||||
<h1 className="title">Nginx + SSL Client Certificate Verification: Manage access to a site</h1>
|
||||
<p className="paragraph">Access control is a fundamental part of security. Most entities rely on
|
||||
the combination of username and password, sometimes with additional multi-factor authentication
|
||||
to improve security. Some entities also use the SSL client certificate verification to manage access
|
||||
to specific resources. One of the use cases where SSL client certificate verification fits perfectly is
|
||||
managing access to internet-facing development or staging servers. In this post, I'll share how
|
||||
to set up the certificates and configure nginx to verify users based on their certificates.</p>
|
||||
<h1>Preparing the certificates</h1>
|
||||
<p className="paragraph">There are two certificates we are going to create. The first one is the root
|
||||
certificate. It will be placed in the Nginx server. The second one is the client certificate. It will
|
||||
be installed in the client machine/browsers.</p>
|
||||
<h2>Root CA</h2>
|
||||
<p className="paragraph">For generating a root CA, execute these two steps:</p>
|
||||
<h3>Generate RSA Key</h3>
|
||||
<p className="code">openssl genrsa -aes256 -out ca.key 4096</p>
|
||||
<h3>Create Root CA crt file.</h3>
|
||||
<p className="code">openssl req -new -x509 -days 3650 -key ca.key -out ca.crt</p>
|
||||
<h2>Setup CA configuration</h2>
|
||||
<p className="paragraph">This is an optional step, but if you want to be able to revoke access you
|
||||
previously granted, you need to do this step.</p>
|
||||
<p className="paragraph">Create a file named ca.cnf in the same directory as the ca.key and ca.crt.</p>
|
||||
<p className="code">[ ca ]
|
||||
default_ca = gca
|
||||
|
||||
[ crl_ext ]
|
||||
authorityKeyIdentifier=keyid:always
|
||||
|
||||
[ gca ]
|
||||
dir = ./
|
||||
new_certs_dir = $dir
|
||||
unique_subject = no
|
||||
certificate = $dir/ca.crt
|
||||
database = $dir/certindex
|
||||
private_key = $dir/ca.key
|
||||
serial = $dir/certserial
|
||||
default_days = 365
|
||||
default_md = sha256
|
||||
policy = gca_policy
|
||||
x509_extensions = gca_extensions
|
||||
crlnumber = $dir/crlnumber
|
||||
default_crl_days = 365
|
||||
|
||||
[ gca_policy ]
|
||||
commonName = supplied
|
||||
stateOrProvinceName = supplied
|
||||
countryName = optional
|
||||
emailAddress = optional
|
||||
organizationName = supplied
|
||||
organizationUnitName = optional
|
||||
|
||||
[ gca_extensions ]
|
||||
basicConstraints = CA:false
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always
|
||||
keyUsage = digitalSignature,keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
crlDistributionPoints = URI:http://example.com/root.crl
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[ alt_names ]
|
||||
DNS.1 = example.com
|
||||
DNS.2 = *.example.com</p>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
<h1 class="title">Nginx + SSL Client Certificate Verification: Manage access to a site</h1>
|
||||
<p class="paragraph">Access control is a fundamental part of security. Most entities rely on
|
||||
the combination of username and password, sometimes with additional multi-factor authentication
|
||||
to improve security. Some entities also use the SSL client certificate verification to manage access
|
||||
to specific resources. One of the use cases where SSL client certificate verification fits perfectly is
|
||||
managing access to internet-facing development or staging servers. In this post, I'll share how
|
||||
to set up the certificates and configure nginx to verify users based on their certificates.</p>
|
||||
<h1>Preparing the certificates</h1>
|
||||
<p class="paragraph">There are two certificates we are going to create. The first one is the root
|
||||
certificate. It will be placed in the Nginx server. The second one is the client certificate. It will
|
||||
be installed in the client machine/browsers.</p>
|
||||
<h2>Root CA</h2>
|
||||
<p class="paragraph">For generating a root CA, execute these two steps:</p>
|
||||
<h3>Generate RSA Key</h3>
|
||||
<code lang="shell">openssl genrsa -aes256 -out ca.key 4096</code>
|
||||
<h3>Create Root CA crt file.</h3>
|
||||
<code lang="shell">openssl req -new -x509 -days 3650 -key ca.key -out ca.crt</code>
|
||||
<h2>Setup CA configuration</h2>
|
||||
<p class="paragraph">This is an optional step, but if you want to be able to revoke access you
|
||||
previously granted, you need to do this step.</p>
|
||||
<p class="paragraph">Create a file named ca.cnf in the same directory as the ca.key and ca.crt.</p>
|
||||
<code lang="ini" class="linenumber">[ ca ]
|
||||
default_ca = gca
|
||||
|
||||
[ crl_ext ]
|
||||
authorityKeyIdentifier=keyid:always
|
||||
|
||||
[ gca ]
|
||||
dir = ./
|
||||
new_certs_dir = $dir
|
||||
unique_subject = no
|
||||
certificate = $dir/ca.crt
|
||||
database = $dir/certindex
|
||||
; mark(1[9:11]) dimgrey
|
||||
private_key = $dir/ca.key
|
||||
serial = $dir/certserial
|
||||
default_days = 365
|
||||
default_md = sha256
|
||||
policy = gca_policy
|
||||
x509_extensions = gca_extensions
|
||||
crlnumber = $dir/crlnumber
|
||||
default_crl_days = 365
|
||||
|
||||
[ gca_policy ]
|
||||
commonName = supplied
|
||||
stateOrProvinceName = supplied
|
||||
countryName = optional
|
||||
emailAddress = optional
|
||||
organizationName = supplied
|
||||
organizationUnitName = optional
|
||||
|
||||
[ gca_extensions ]
|
||||
basicConstraints = CA:false
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always
|
||||
keyUsage = digitalSignature,keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
crlDistributionPoints = URI:http://example.com/root.crl
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[ alt_names ]
|
||||
DNS.1 = example.com
|
||||
DNS.2 = *.example.com</code>
|
||||
<p class="paragraph">Initialize an empty file for the CA database.</p>
|
||||
<code lang="shell">touch certindex</code>
|
||||
<p class="paragraph">Initialize value for certserial and crlnumber</p>
|
||||
<code lang="shell">echo 01 > certserial
|
||||
echo 01 > crlnumber</code>
|
||||
<h2>User Certificates</h2>
|
||||
<h3>Generate the user RSA key.</h3>
|
||||
<code lang="shell">openssl genrsa -aes256 -out client01/user.key 4096</code>
|
||||
<h3>Create Certificate-Signing Request (CSR)</h3>
|
||||
<code lang="shell">openssl req -new -key client01/user.key -out client01/user.csr</code>
|
||||
<h3>Sign the CSR.</h3>
|
||||
<p class="paragraph">If you did the setup CA configuration step, sign the CSR file by running this command.</p>
|
||||
<code lang="shell">openssl ca -config ca.cnf -in client01/user.csr -out client01/user.crt</code>
|
||||
<p class="paragraph">If you skipped the setup CA configuration step, sign the CSR file by running this command.</p>
|
||||
<code lang="shell">openssl x509 -req -days 365 -in client01/user.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client01/user.crt</code>
|
||||
<h3>Convert the crt file to pfx/p12 file.</h3>
|
||||
<p class="paragraph">Most of the time, browsers/client machines only accept a certificate in the pfx format. Run this
|
||||
command to convert the crt file to the pfx/p12 format.</p>
|
||||
<code lang="shell">openssl pkcs12 -export -out client01/user.pfx -inkey client01/user.key -in client01/user.crt -certfile ca.crt</code>
|
||||
<p class="paragraph">You'll be prompted to enter an export password. You must input the exact password when adding
|
||||
the certificate to a browser.</p>
|
||||
<br/>
|
||||
<h1>Setting up nginx with client certificates verification</h1>
|
||||
<p class="paragraph">Add these lines to a server block in your nginx configuration</p>
|
||||
<code lang="shell" class="linenumber">ssl_client_certificate /path/to/client/verfication/ca.crt;
|
||||
ssl_verify_client optional;
|
||||
ssl_verify_depth 2;</code>
|
||||
<p class="paragraph">You can do location-based access control. Location-based here refers to a location block in your
|
||||
nginx configuration, for example:</p>
|
||||
<code lang="shell"> location /private {
|
||||
# mark(1[13:41]) dimgrey
|
||||
if ($ssl_client_verify != SUCCESS) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
....
|
||||
|
||||
}
|
||||
</code>
|
||||
<p class="paragraph">Here is a complete example of a server block in the nginx configuration</p>
|
||||
<code lang="shell">server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
server_name www.example.com;
|
||||
ssl_certificate /path/to/your/https/certificate.pem;
|
||||
ssl_certificate_key /path/to/your/https/private-key.pem;
|
||||
include snippets/ssl-params.conf;
|
||||
|
||||
# mark(1:3) dimgrey
|
||||
ssl_client_certificate /path/to/client/verification/ca.crt;
|
||||
ssl_verify_client optional;
|
||||
ssl_verify_depth 2;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
location / {
|
||||
# mark(1[13:41]) dimgrey
|
||||
if ($ssl_client_verify != SUCCESS) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
}
|
||||
}</code>
|
||||
<br/>
|
||||
<h1>Adding the User Certificates to the client machine/browsers</h1>
|
|
@ -15,6 +15,7 @@
|
|||
"@types/react": "18.2.22",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"autoprefixer": "10.4.16",
|
||||
"bright": "^0.8.4",
|
||||
"dompurify": "^3.0.6",
|
||||
"eslint": "8.50.0",
|
||||
"eslint-config-next": "13.5.2",
|
||||
|
|
|
@ -23,6 +23,9 @@ dependencies:
|
|||
autoprefixer:
|
||||
specifier: 10.4.16
|
||||
version: 10.4.16(postcss@8.4.30)
|
||||
bright:
|
||||
specifier: ^0.8.4
|
||||
version: 0.8.4(react@18.2.0)
|
||||
dompurify:
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
|
@ -87,6 +90,10 @@ packages:
|
|||
regenerator-runtime: 0.14.0
|
||||
dev: false
|
||||
|
||||
/@code-hike/lighter@0.8.1:
|
||||
resolution: {integrity: sha512-St4rPmB7C2EWmAK1sAbvD3lZeM7UDInVDMjQDzEDsu4Q3B3AqF25vXedQK51U0UO0MCOASgBBdTiNwvJAfIqMQ==}
|
||||
dev: false
|
||||
|
||||
/@eslint-community/eslint-utils@4.4.0(eslint@8.50.0):
|
||||
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
@ -685,6 +692,16 @@ packages:
|
|||
fill-range: 7.0.1
|
||||
dev: false
|
||||
|
||||
/bright@0.8.4(react@18.2.0):
|
||||
resolution: {integrity: sha512-SBlcJje8EDh3lLihqr0W27yHCVpuh3NReziNxZOwUzuaShqq+V/imY+J+AvJdTIOifiCQkyIsgVWKAj5G/eOyg==}
|
||||
peerDependencies:
|
||||
react: ^18
|
||||
dependencies:
|
||||
'@code-hike/lighter': 0.8.1
|
||||
react: 18.2.0
|
||||
server-only: 0.0.1
|
||||
dev: false
|
||||
|
||||
/browserslist@4.21.11:
|
||||
resolution: {integrity: sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
|
@ -2664,6 +2681,10 @@ packages:
|
|||
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
|
||||
dev: false
|
||||
|
||||
/server-only@0.0.1:
|
||||
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
|
||||
dev: false
|
||||
|
||||
/set-function-name@2.0.1:
|
||||
resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
Loading…
Reference in New Issue