13 Commits

Author SHA1 Message Date
cbcf6a731b library version updates 2024-05-09 14:41:42 +10:00
9ad8e1aa9b move the assets to remote location and change blog cover to Next/Image 2024-03-13 14:23:11 +11:00
bea1c3aba3 updated dependency, standalone seems to work
updated pnpm version
added dockerignore
revert the standalone output
2024-02-23 14:56:19 +11:00
5c8a3f56dd WIP: build attempted 2024-02-22 23:42:04 +11:00
a1cba242e9 WIP: updated HTMLReactParserOptions replace function, from complex if else to handler pattern 2024-02-22 14:07:36 +11:00
a607a4528e WIP: testing bright and resume copying content 2023-10-23 22:32:15 +11:00
cf0579023e wip: test versions of pnpm and nodejs 2023-10-22 15:24:59 +11:00
6d3dab582a wip: move workstation 2023-10-21 10:17:10 +11:00
08db8ef124 updated font reference and clean up 2023-09-28 20:54:59 +10:00
fca3d8bcec initialized structure 2023-09-26 19:48:47 +10:00
7e6a2b0300 upper body finished 2023-09-26 18:29:37 +10:00
2dbf116ddf upper body finished 2023-09-25 22:17:46 +10:00
bfed20c1c1 migrated 2023-09-25 20:43:46 +10:00
50 changed files with 2376 additions and 981 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
**/node_modules
.git
.gitignore
.local
.env*

View File

@@ -1,3 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
"extends": "next/core-web-vitals"
}

13
.gitignore vendored
View File

@@ -3,12 +3,9 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
.pnp.js
/.local/
# testing
/coverage
@@ -29,8 +26,8 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
# local env files
.env.development
# vercel
.vercel

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

29
.idea/dataSources.local.xml generated Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="WS-241.15989.105">
<data-source name="devblog@mysql-server" uuid="32913bc6-cafd-416c-b070-eee7c73cf755">
<database-info product="MySQL" version="8.3.0" jdbc-version="4.2" driver-name="MySQL Connector/J" driver-version="mysql-connector-java-8.0.25 (Revision: 08be9e9b4cba6aa115f9b27b215887af40b159e0)" dbms="MYSQL" exact-version="8.3.0" exact-driver-version="8.0">
<extra-name-characters>#@</extra-name-characters>
<identifier-quote-string>`</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="exact" quoted-identifiers="exact" />
<secret-storage>master_key</secret-storage>
<user-name>devblog</user-name>
<schema-mapping>
<introspection-scope>
<node kind="schema">
<name qname="@" />
<name qname="devblog" />
</node>
</introspection-scope>
</schema-mapping>
<ssl-config use-ide-store="true" use-java-store="true" use-system-store="true">
<ca-cert>$USER_HOME$/Documents/db-ssl/ca.crt</ca-cert>
<client-cert>$USER_HOME$/Documents/db-ssl/mbp.crt</client-cert>
<client-key>$USER_HOME$/Documents/db-ssl/mbp.key</client-key>
<enabled>true</enabled>
<mode>VERIFY_FULL</mode>
</ssl-config>
</data-source>
</component>
</project>

12
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="devblog@mysql-server" uuid="32913bc6-cafd-416c-b070-eee7c73cf755">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://mysql-server.suyono.dev:13306/devblog</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/nextts.iml" filepath="$PROJECT_DIR$/.idea/nextts.iml" />
</modules>
</component>
</project>

12
.idea/nextts.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

7
.idea/prettier.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="MANUAL" />
<option name="myRunOnReformat" value="true" />
</component>
</project>

6
.idea/sqldialects.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="MySQL" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

2
.tool-versions Normal file
View File

@@ -0,0 +1,2 @@
nodejs 20.12.2
pnpm 9.0.6

37
Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
FROM node:lts-alpine as base
FROM base as builder
USER 1000:1000
ADD --chown=1000:1000 . /home/node/nextts
WORKDIR /home/node/nextts
RUN wget -qO- https://get.pnpm.io/install.sh | PNPM_VERSION="8.15.3" ENV="/home/node/.shrc" SHELL="$(which sh)" sh -
ENV PATH=/home/node/.local/share/pnpm:$PATH
RUN pnpm install && pnpm run build
FROM base as runtime
RUN npm install -g pm2
COPY --from=builder /home/node/nextts/public /home/node/nextts/public
COPY --from=builder /home/node/nextts/.next/standalone /home/node/nextts
COPY --from=builder /home/node/nextts/.next/static /home/node/nextts/.next/static
ADD --chown=1000:1000 pm2.config.js /home/node/nextts/
ADD --chown=1000:1000 dummies /home/node/nextts/dummies
RUN chown -R 1000:1000 /home/node/nextts
USER 1000:1000
WORKDIR /home/node/nextts
ENV PORT 3000
ENV NODE_ENV production
ENV HOME /home/node
ENV HOSTNAME "0.0.0.0"
RUN wget -qO- https://get.pnpm.io/install.sh | PNPM_VERSION="8.15.3" ENV="/home/node/.shrc" SHELL="$(which sh)" sh -
ENV PATH=/home/node/.local/share/pnpm:$PATH
RUN pnpm install
CMD ["pm2-runtime", "pm2.config.js"]
#CMD ["node", "server.js"]

View File

@@ -1,4 +1,4 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
@@ -18,7 +18,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
@@ -27,10 +27,10 @@ To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

7
app/about/page.tsx Normal file
View File

@@ -0,0 +1,7 @@
export default function About() {
return(
<div className={`flex flex-col`}>
<p>About</p>
</div>
)
}

7
app/blog/page.tsx Normal file
View File

@@ -0,0 +1,7 @@
export default function Blog() {
return(
<div className={`flex flex-col`}>
<p>Blog Post List</p>
</div>
)
}

28
app/dbcheck/page.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { getPromisePool } from "@/backend/db";
import {RowDataPacket} from "mysql2";
async function query() {
try {
const [rows, fields] = await getPromisePool().query<RowDataPacket[]>('select slug from post limit 1;')
return(rows[0]['slug'] as string)
} catch (e) {
console.log(e)
return('something went wrong')
}
}
export default async function DbCheck({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined }}) {
let flag = "empty";
if (typeof searchParams["flag"] === 'string') {
flag = searchParams["flag"]
}
return(
<div className={`flex flex-col`}>
<p>Env: { process.env.MYSQL_HOST }</p>
<p>Result: { await query() }</p>
<p>Flag: { flag }</p>
</div>
)
}

23
app/fonts.ts Normal file
View File

@@ -0,0 +1,23 @@
import { Raleway, Syne, Questrial, Nunito_Sans } from "next/font/google";
export const raleway = Raleway({
subsets: ['latin'],
display: "swap",
})
export const syne = Syne({
subsets: ['latin'],
display: "swap",
})
export const questrial = Questrial({
subsets: ['latin'],
display: "swap",
weight: ['400'],
})
export const nunito_sans = Nunito_Sans({
subsets: ['latin'],
display: "swap",
}
)

Binary file not shown.

Binary file not shown.

View File

@@ -1,21 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}

27
app/globals.css.orig Normal file
View File

@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}

View File

@@ -1,17 +1,11 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import BlogHeader from "@/components/blogHeader";
import BlogFooter from "@/components/blogFooter";
import React from "react";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
@@ -20,15 +14,17 @@ export const metadata: Metadata = {
export default function RootLayout({
children,
}: Readonly<{
}: {
children: React.ReactNode;
}>) {
}) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<body className={inter.className}>
<div className={`flex flex-col bg-white`}>
<BlogHeader />
{children}
<BlogFooter />
</div>
</body>
</html>
);

View File

@@ -1,101 +1,76 @@
import Image from "next/image";
import Link from "next/link";
import { raleway, syne, questrial } from "@/app/fonts";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
<div className={`flex flex-col`}>
<div className={`grid grid-rows-1 grid-cols-1 justify-items-center`}>
<Image
src={`https://assets.suyono.me/placeholder.webp`}
alt={`blog cover`}
className={`object-cover col-start-1 row-start-1 w-screen h-192 z-0`}
width={1581}
height={759}
/>
<div className={`flex flex-col-reverse col-start-1 row-start-1 w-screen`}>
<div className={`bg-neutral-100 bg-opacity-30 flex flex-col py-10 z-10`}>
<p className={`${raleway.className} text-white text-center text-7xl font-thin mb-6`}>
SUYONO
</p>
<p className={`${raleway.className} text-white text-center font-thin text-xl mb-10`}>
A Tech Archive
</p>
</div>
</div>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
<div className={`flex flex-row justify-center my-8`}>
<div className={`border border-slate-100 flex flex-col`}>
<Link
href="/post/nginx-ssl-client-certificate-verification-manage-access-to-a-site"
className={`flex flex-row max-w-4xl items-center`}
>
<Image
src="https://assets.suyono.me/pthumb.webp"
alt="post thumbnail"
width={454}
height={341}
/>
<div className={`flex flex-col mx-10`}>
<p className={`${syne.className} text-2xl`}>
Nginx + SSL Client Certificate Verification: Manage Access to a
site
</p>
<p className={`${questrial.className} line-clamp-3 mt-4`}>
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&apos;ll share how to set up the certificates and
configure nginx to verify users based on their certificates.
</p>
</div>
</Link>
</div>
</div>
<div className={`flex flex-row bg-teal-50 justify-center`}>
<div className={`max-w-4xl py-28 px-10`}>
<p className={`text-3xl ${raleway.className}`}>Hi There</p>
<p className={`text-base ${raleway.className} my-4`}>
a new take on experience is the best teacher
</p>
<p className={`${raleway.className} text-sm`}>
I started this blog as an archive of my experiences and knowledge.
By writing them out, I hope it will help me unlearn and relearn the
various knowledge and skills I&apos;ve accumulated. I hope the
articles, source code examples, and server config examples I wrote
will help you somehow. Read on and enjoy!
</p>
</div>
</div>
</div>
);
}

View File

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

45
app/post/[slug]/page.tsx Normal file
View File

@@ -0,0 +1,45 @@
import { getPost } from "@/backend/post";
import DOMPurify from "dompurify";
import { JSDOM } from "jsdom";
import parse, {
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) => {
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
}
}
}
},
};
export default async function Post({ params }: { params: { slug: string } }) {
let content;
const dummySlug = await DummyPostSlug();
if (dummySlug === params.slug) {
content = await DummyPostString();
// console.log(content);
} else {
content = await getPost(params.slug);
}
content = DOMPurify(new JSDOM("<!DOCTYPE html>").window).sanitize(content);
// console.log(content)
const elem = parse(content, options);
return (
<div className={`flex flex-col`}>
{elem}
</div>
);
}

42
backend/db.ts Normal file
View File

@@ -0,0 +1,42 @@
import mysql, { PoolOptions, Pool } from "mysql2";
import { Pool as pPool } from "mysql2/promise"
import * as fs from 'fs';
import * as appEnv from "./env";
let pool: Pool | undefined;
let promisePool: pPool | undefined;
export function getPool(): Pool {
const access: PoolOptions = {
host: appEnv.getMysqlHost(),
port: appEnv.getMysqlPort(),
user: appEnv.getMysqlUser(),
password: appEnv.getMysqlPassword(),
database: appEnv.getMysqlDatabase(),
waitForConnections: true,
connectionLimit: 10,
maxIdle: 10,
idleTimeout: 60000,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 0,
ssl: {
ca: fs.readFileSync(appEnv.getMysqlSslCaFile()),
key: fs.readFileSync(appEnv.getMysqlSslKeyFile()),
cert: fs.readFileSync(appEnv.getMysqlSslCertFile())
}
}
if (typeof pool === 'undefined') {
pool = mysql.createPool(access)
}
return pool
}
export function getPromisePool(): pPool {
if (typeof promisePool === 'undefined') {
promisePool = getPool().promise()
}
return promisePool
}

63
backend/env.ts Normal file
View File

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

13
backend/post.ts Normal file
View File

@@ -0,0 +1,13 @@
import { RowDataPacket } from "mysql2";
import { getPromisePool } from "@/backend/db";
export async function getPost(slug: string): Promise<string> {
try {
const [rows, fields] = await getPromisePool().query<RowDataPacket[]>(
'select content from post where slug = ?', [slug])
return rows[0]['content']
} catch (e) {
console.log(e)
throw e
}
}

View File

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

15
bright-extension/mark.tsx Normal file
View File

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

13
components/blogFooter.tsx Normal file
View File

@@ -0,0 +1,13 @@
import {raleway} from "@/app/fonts";
export default function BlogFooter() {
return (
<div>
<p className={`${raleway.className} text-center text-xl my-10`}>Suyono</p>
<p className={`${raleway.className} text-center`}>suyono3484@gmail.com</p>
<p className={`${raleway.className} text-center mt-20 mb-10`}>
&copy;2023 by Suyono. Built using Next.js
</p>
</div>
);
}

25
components/blogHeader.tsx Normal file
View File

@@ -0,0 +1,25 @@
import Link from "next/link";
import { raleway }from "@/app/fonts";
export default function BlogHeader() {
return(
<div>
<div className="ml-20 py-8">
<p className={`${raleway.className} text-2xl font-thin`}>SUYONO</p>
</div>
<div className="bg-gray-100">
<div className="flex flex-row ml-20">
<Link href="/" className={`${raleway.className} m-2 font-thin text-sm`}>
Home
</Link>
<Link href="/about" className={`${raleway.className} m-2 font-thin text-sm`}>
About
</Link>
<Link href="/blog" className={`${raleway.className} m-2 font-thin text-sm`}>
Blog
</Link>
</div>
</div>
</div>
)
}

13
components/dummyPost.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { promises as fsp } from 'fs'
export async function DummyPostString() {
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"
}

130
dummies/test1.html Normal file
View File

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

19
next.config.js Normal file
View File

@@ -0,0 +1,19 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
webpack: (config) => {
config.externals = [...config.externals, "jsdom"];
return config;
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "assets.suyono.me",
pathname: "/**"
},
]
}
}
module.exports = nextConfig

View File

@@ -1,7 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

View File

@@ -1,5 +1,5 @@
{
"name": "blogts2",
"name": "nextts",
"version": "0.1.0",
"private": true,
"scripts": {
@@ -9,19 +9,29 @@
"lint": "next lint"
},
"dependencies": {
"react": "19.0.0-rc-66855b96-20241106",
"react-dom": "19.0.0-rc-66855b96-20241106",
"next": "15.0.3"
"@types/dompurify": "^3.0.5",
"@types/jsdom": "^21.1.6",
"@types/node": "20.6.5",
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.16",
"bright": "^0.8.5",
"dompurify": "^3.1.2",
"eslint": "8.57.0",
"eslint-config-next": "14.2.3",
"html-react-parser": "^4.2.10",
"jsdom": "^22.1.0",
"mysql2": "^3.9.7",
"next": "14.2.3",
"postcss": "8.4.30",
"react": "18.3.1",
"react-dom": "18.3.1",
"redis": "^4.6.13",
"sharp": "^0.33.3",
"tailwindcss": "3.3.3",
"typescript": "5.2.2"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "15.0.3"
},
"packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab"
"prettier": "3.0.3"
}
}

7
pm2.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
apps: [{
script: "server.js",
instances: 4,
exec_mode: "cluster"
}]
}

2291
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

View File

@@ -1,18 +1,26 @@
import type { Config } from "tailwindcss";
import type { Config } from 'tailwindcss'
export default {
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
width: {
'224': '56rem',
},
height: {
'192': '48rem',
}
},
},
plugins: [],
} satisfies Config;
}
export default config

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2017",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,