Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e64170551 | |||
| 29892a915d | |||
| 1340f2029a | |||
| ff6886e309 | |||
| 0d09e0d44f | |||
| 7252456dd4 | |||
| fe41df4244 | |||
| 49e9fa8bc6 | |||
| a828c62e71 | |||
| 552c99bfe7 | |||
| d7a9fb6a8c | |||
| 15b7b30b08 | |||
| d709f6657a | |||
| 0ca7a97d26 | |||
| cbcf6a731b | |||
| 9ad8e1aa9b | |||
| bea1c3aba3 | |||
| 5c8a3f56dd | |||
| a1cba242e9 | |||
| a607a4528e | |||
| cf0579023e | |||
| 6d3dab582a | |||
| 08db8ef124 | |||
| fca3d8bcec | |||
| 7e6a2b0300 | |||
| 2dbf116ddf | |||
| bfed20c1c1 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
**/node_modules
|
||||
.git
|
||||
.gitignore
|
||||
.local
|
||||
.env*
|
||||
10
.env
Normal file
10
.env
Normal file
@@ -0,0 +1,10 @@
|
||||
DUMMY_HTML_DIR=./development-test-data-dir/
|
||||
ASSETS_DOMAIN=assets.suyono.me
|
||||
MYSQL_HOST=db.host
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=dbuser
|
||||
MYSQL_PASSWORD=dbpasswd
|
||||
MYSQL_DATABASE=dbname
|
||||
MYSQL_SSL_CA=/path/to/ca.file
|
||||
MYSQL_SSL_CERT=/path/to/cert.file
|
||||
MYSQL_SSL_KEY=/path/to/key.file
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,6 +5,8 @@
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
/.local/
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
@@ -25,7 +27,8 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env.local
|
||||
.env.development.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
30
.idea/dataSources.local.xml
generated
Normal file
30
.idea/dataSources.local.xml
generated
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="dataSourceStorageLocal" created-in="WS-241.17011.90">
|
||||
<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-j-8.2.0 (Revision: 06a1f724497fd81c6a659131fda822c9e5085b6c)" dbms="MYSQL" exact-version="8.3.0" exact-driver-version="8.2">
|
||||
<extra-name-characters>#@</extra-name-characters>
|
||||
<identifier-quote-string>`</identifier-quote-string>
|
||||
<jdbc-catalog-is-schema>true</jdbc-catalog-is-schema>
|
||||
</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
12
.idea/dataSources.xml
generated
Normal 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>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
6
.idea/jsLibraryMappings.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
12
.idea/nextts.iml
generated
Normal 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
7
.idea/prettier.xml
generated
Normal 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
6
.idea/sqldialects.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
37
Dockerfile
Normal file
37
Dockerfile
Normal 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"]
|
||||
7
app/about/page.tsx
Normal file
7
app/about/page.tsx
Normal 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
7
app/blog/page.tsx
Normal 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
28
app/dbcheck/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
41
app/fonts.ts
Normal file
41
app/fonts.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
Raleway,
|
||||
Syne,
|
||||
Questrial,
|
||||
Roboto,
|
||||
Nunito_Sans,
|
||||
Open_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 roboto = Roboto({
|
||||
subsets: ['latin'],
|
||||
weight: ['400'],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const openSans = Open_Sans({
|
||||
subsets: ['latin'],
|
||||
display: "swap",
|
||||
})
|
||||
|
||||
export const nunito = Nunito_Sans({
|
||||
subsets: ['latin'],
|
||||
weight: ['300'],
|
||||
display: "swap",
|
||||
})
|
||||
@@ -1,27 +1,5 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/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));
|
||||
}
|
||||
@import "highlight.js/styles/base16/atelier-forest-light.min.css";
|
||||
|
||||
27
app/globals.css.orig
Normal file
27
app/globals.css.orig
Normal 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));
|
||||
}
|
||||
@@ -1,22 +1,28 @@
|
||||
import './globals.css'
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
import "./globals.css";
|
||||
import type { Metadata } from "next";
|
||||
import BlogHeader from "@/components/blogHeader";
|
||||
import BlogFooter from "@/components/blogFooter";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<body>
|
||||
<div className={`flex flex-col bg-white`}>
|
||||
<BlogHeader />
|
||||
{children}
|
||||
<BlogFooter />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
7
app/mark/page.tsx
Normal file
7
app/mark/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { MarkPostString } from '@/components/dummyPost';
|
||||
import { content } from '@/components/post';
|
||||
|
||||
export default async function Mark() {
|
||||
let postContent = await MarkPostString();
|
||||
return content(postContent);
|
||||
}
|
||||
135
app/page.tsx
135
app/page.tsx
@@ -1,113 +1,46 @@
|
||||
import Image from 'next/image'
|
||||
import Image from "next/image";
|
||||
import { getAssetsDomain } from "@/backend/env"
|
||||
import { raleway, syne, questrial } from "@/app/fonts";
|
||||
import { HomePostList } from "@/components/homePostList";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
|
||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
||||
Get started by editing
|
||||
<code className="font-mono font-bold">app/page.tsx</code>
|
||||
</p>
|
||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
|
||||
<a
|
||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{' '}
|
||||
<div className={`flex flex-col`}>
|
||||
<div className={`grid grid-rows-1 grid-cols-1 justify-items-center`}>
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className="dark:invert"
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
src={`https://${getAssetsDomain()}/placeholder.webp`}
|
||||
alt={`blog cover`}
|
||||
className={`object-cover col-start-1 row-start-1 w-screen h-192 z-0`}
|
||||
width={1581}
|
||||
height={759}
|
||||
/>
|
||||
</a>
|
||||
<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 className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
|
||||
<Image
|
||||
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Docs{' '}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Find in-depth information about Next.js features and API.
|
||||
<HomePostList className={`flex flex-col mx-auto my-8`} />
|
||||
<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>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Learn{' '}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Learn about Next.js in an interactive course with quizzes!
|
||||
<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've accumulated. I hope the
|
||||
articles, source code examples, and server config examples I wrote
|
||||
will help you somehow. Read on and enjoy!
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Templates{' '}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Explore the Next.js 13 playground.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Deploy{' '}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
25
app/post/[slug]/page.tsx
Normal file
25
app/post/[slug]/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { getPost } from '@/backend/post';
|
||||
import { content } from '@/components/post'
|
||||
|
||||
export default async function Post({ params }: { params: { slug: string } }) {
|
||||
// let content;
|
||||
//
|
||||
// const dummySlug = DummyPostSlug();
|
||||
// if (dummySlug === params.slug) {
|
||||
// content = await DummyPostString();
|
||||
// } else {
|
||||
// content = await getPost(params.slug);
|
||||
// }
|
||||
//
|
||||
// content = DOMPurify(new JSDOM("<!DOCTYPE html>").window).sanitize(content);
|
||||
// const elem = parse(content, options);
|
||||
//
|
||||
// return (
|
||||
// <div className={`flex flex-col`}>
|
||||
// {elem}
|
||||
// </div>
|
||||
// );
|
||||
|
||||
let postContent :string = await getPost(params.slug);
|
||||
return(content(postContent));
|
||||
}
|
||||
42
backend/db.ts
Normal file
42
backend/db.ts
Normal 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 "@/backend/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
|
||||
}
|
||||
71
backend/env.ts
Normal file
71
backend/env.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
export function getAssetsDomain(): string {
|
||||
if (typeof process.env.ASSETS_DOMAIN === 'undefined') {
|
||||
throw new Error("missing env ASSETS_DOMAIN");
|
||||
}
|
||||
|
||||
return process.env.ASSETS_DOMAIN;
|
||||
}
|
||||
|
||||
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
13
backend/post.ts
Normal 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
|
||||
}
|
||||
}
|
||||
13
components/blogFooter.tsx
Normal file
13
components/blogFooter.tsx
Normal 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`}>
|
||||
©2023 by Suyono. Built using Next.js
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
components/blogHeader.tsx
Normal file
25
components/blogHeader.tsx
Normal 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
13
components/dummyPost.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
export async function MarkPostString() {
|
||||
let path = ""
|
||||
if ('DUMMY_HTML_DIR' in process.env && typeof process.env.DUMMY_HTML_DIR === "string") {
|
||||
path = process.env.DUMMY_HTML_DIR + "test1.md";
|
||||
}
|
||||
return await readFile(path, "utf-8")
|
||||
}
|
||||
|
||||
export function DummyPostSlug() {
|
||||
return "dummy-post"
|
||||
}
|
||||
47
components/homePostList.tsx
Normal file
47
components/homePostList.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import {getAssetsDomain} from "@/backend/env";
|
||||
import {questrial, syne} from "@/app/fonts";
|
||||
import { RowDataPacket } from "mysql2";
|
||||
import { getPromisePool } from "@/backend/db";
|
||||
|
||||
interface post extends RowDataPacket {
|
||||
id: number,
|
||||
slug: string,
|
||||
title: string,
|
||||
preview: string,
|
||||
cover_url: string,
|
||||
cover_alt: string,
|
||||
preview_cover_height: number,
|
||||
preview_cover_width: number,
|
||||
}
|
||||
|
||||
export type HomePostListProps = {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export async function HomePostList(props :HomePostListProps) {
|
||||
const [posts] = await getPromisePool().query<post[]>(
|
||||
'SELECT * FROM post LIMIT 10;'
|
||||
);
|
||||
return (
|
||||
<div className={props.className}>
|
||||
{posts.map(post => (
|
||||
<div key={post.id} className={`border border-slate-100 flex flex-col`}>
|
||||
<Link
|
||||
href={`/post/${post.slug}`}
|
||||
className={`flex flex-row max-w-4xl items-center`} >
|
||||
<Image src={post.cover_url}
|
||||
alt={post.cover_alt}
|
||||
height={post.preview_cover_height}
|
||||
width={post.preview_cover_width} />
|
||||
<div className={`flex flex-col mx-10`}>
|
||||
<p className={`${syne.className} text-2xl`}>{post.title}</p>
|
||||
<p className={`${questrial.className} line-clamp-3 mt-4`}>{post.preview}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
120
components/post.tsx
Normal file
120
components/post.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { unified } from 'unified';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkParse from 'remark-parse';
|
||||
import remarkRehype, { Options as RemarkRehypeOptions } from 'remark-rehype';
|
||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
||||
import rehypeHighlight, { Options as RehypeHighlightOptions } from 'rehype-highlight';
|
||||
import rehypeReact, { Options as RehypeReactOptions } from 'rehype-react';
|
||||
import { all } from 'lowlight';
|
||||
import { Fragment, jsx, jsxs } from 'react/jsx-runtime';
|
||||
import { raleway, roboto, nunito } from "@/app/fonts";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import Image from "next/image";
|
||||
import { getAssetsDomain } from "@/backend/env";
|
||||
|
||||
export async function content(content: string) {
|
||||
let result = await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype, { allowDangerousHtml: true } as RemarkRehypeOptions)
|
||||
.use(rehypeRaw)
|
||||
.use(rehypeSanitize, {
|
||||
...defaultSchema,
|
||||
tagNames: [
|
||||
...(defaultSchema.tagNames || []),
|
||||
'figure',
|
||||
'figcaption',
|
||||
]
|
||||
})
|
||||
.use(rehypeHighlight, {
|
||||
languages: all,
|
||||
} as RehypeHighlightOptions)
|
||||
.use(rehypeReact, {
|
||||
Fragment: Fragment,
|
||||
jsx: jsx,
|
||||
jsxs: jsxs,
|
||||
passNode: true,
|
||||
components: {
|
||||
h1: (props) => (
|
||||
<h1 className={`${raleway.className} mx-auto w-224 text-4xl`}>
|
||||
{props.children}
|
||||
</h1>
|
||||
),
|
||||
h2: (props) => (
|
||||
<h2 className={`${raleway.className} mx-auto mt-6 w-224 text-3xl`}>
|
||||
{props.children}
|
||||
</h2>
|
||||
),
|
||||
h3: (props) => (
|
||||
<h3 className={`${raleway.className} mx-auto mt-4 w-224 text-2xl`}>
|
||||
{props.children}
|
||||
</h3>
|
||||
),
|
||||
h4: (props) => (
|
||||
<h4 className={`${raleway.className} mx-auto mt-3 w-224 text-xl`}>
|
||||
{props.children}
|
||||
</h4>
|
||||
),
|
||||
p: (props) => (
|
||||
<p className={`${nunito.className} mx-auto mt-2 w-224`}>
|
||||
{props.children}
|
||||
</p>
|
||||
),
|
||||
pre: (props) => (
|
||||
<div className={`w-224 mx-auto mt-2`}>
|
||||
<pre>{props.children}</pre>
|
||||
</div>
|
||||
),
|
||||
hr: (props) => <hr className={`mx-auto w-224 mt-6`} />,
|
||||
img: (props) => {
|
||||
let src: string = "";
|
||||
let alt: string = "";
|
||||
|
||||
if (typeof props.src === "undefined") {
|
||||
src = `https://${getAssetsDomain()}/broken-image.svg`;
|
||||
} else {
|
||||
src = props.src;
|
||||
}
|
||||
|
||||
if (typeof props.alt === "string") {
|
||||
alt = props.alt;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof props.width === "undefined" ||
|
||||
typeof props.height === "undefined"
|
||||
) {
|
||||
return (
|
||||
<Image src={src} alt={alt} className={`mx-auto`} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={`mx-auto`}
|
||||
width={
|
||||
typeof props.width === "string"
|
||||
? parseInt(props.width, 10)
|
||||
: props.width
|
||||
}
|
||||
height={
|
||||
typeof props.height === "string"
|
||||
? parseInt(props.height, 10)
|
||||
: props.height
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
figcaption: (props) => (
|
||||
<figcaption className={`${nunito.className} text-center mb-6`}>
|
||||
{props.children}
|
||||
</figcaption>
|
||||
),
|
||||
},
|
||||
} as RehypeReactOptions)
|
||||
.process(content);
|
||||
|
||||
return result.result;
|
||||
}
|
||||
130
dummies/test1.html
Normal file
130
dummies/test1.html
Normal 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'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>
|
||||
337
dummies/test1.md
Normal file
337
dummies/test1.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# Nginx + SSL Client Certificate Verification: Manage access to a site
|
||||
|
||||
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.
|
||||
|
||||
## Preparing the certificates
|
||||
|
||||
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.
|
||||
|
||||
### Root CA
|
||||
|
||||
For generating a root CA, execute these two steps:
|
||||
|
||||
#### Generate RSA Key
|
||||
|
||||
```sh
|
||||
openssl genrsa -aes256 -out ca.key 4096
|
||||
```
|
||||
|
||||
#### Create Root CA file
|
||||
|
||||
```sh
|
||||
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt
|
||||
```
|
||||
|
||||
### Setup CA configuration
|
||||
|
||||
This is an optional step, but if you want to be able to revoke access you previously granted, you need to do this step.
|
||||
Create a file named ca.cnf in the same directory as the ca.key and ca.crt.
|
||||
|
||||
```ini
|
||||
[ 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
|
||||
```
|
||||
|
||||
Initialize an empty file for the CA database.
|
||||
|
||||
```sh
|
||||
touch certindex
|
||||
```
|
||||
|
||||
Initialize value for certserial and crlnumber
|
||||
|
||||
```sh
|
||||
echo 01 > certserial
|
||||
echo 01 > crlnumber
|
||||
```
|
||||
|
||||
### User Certificates
|
||||
|
||||
#### Generate the user RSA key
|
||||
|
||||
```sh
|
||||
openssl genrsa -aes256 -out client01/user.key 4096
|
||||
```
|
||||
|
||||
#### Create Certificate-Signing Request (CSR)
|
||||
|
||||
```sh
|
||||
openssl req -new -key client01/user.key -out client01/user.csr
|
||||
```
|
||||
|
||||
#### Sign the CSR
|
||||
|
||||
If you did the setup CA configuration step, sign the CSR file by running this command.
|
||||
|
||||
```sh
|
||||
openssl ca -config ca.cnf -in client01/user.csr -out client01/user.crt
|
||||
```
|
||||
|
||||
If you skipped the setup CA configuration step, sign the CSR file by running this command.
|
||||
|
||||
```sh
|
||||
openssl x509 -req -days 365 -in client01/user.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client01/user.crt
|
||||
```
|
||||
|
||||
#### Convert the crt file to pfx/p12 file
|
||||
|
||||
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
|
||||
|
||||
```sh
|
||||
openssl pkcs12 -export -out client01/user.pfx -inkey client01/user.key -in client01/user.crt -certfile ca.crt
|
||||
```
|
||||
|
||||
You'll be prompted to enter an export password. You must input the exact password when adding
|
||||
the certificate to a browser.
|
||||
|
||||
---
|
||||
|
||||
## Setting up nginx with client certificates verification
|
||||
|
||||
Add these lines to a server block in your nginx configuration
|
||||
|
||||
```nginx
|
||||
ssl_client_certificate /path/to/client/verfication/ca.crt;
|
||||
ssl_verify_client optional;
|
||||
ssl_verify_depth 2;
|
||||
```
|
||||
|
||||
You can do location-based access control. Location-based here refers to a location block in your
|
||||
nginx configuration, for example:
|
||||
|
||||
```nginx
|
||||
location /private {
|
||||
if ($ssl_client_verify != SUCCESS) { # add this condition
|
||||
return 403; # to make nginx return 403
|
||||
} # when the client has no valid certificate
|
||||
|
||||
....
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Here is a complete example of a server block in the nginx configuration
|
||||
|
||||
```nginx
|
||||
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;
|
||||
|
||||
# the folowing three lines make nginx verify client certificate
|
||||
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 / {
|
||||
if ($ssl_client_verify != SUCCESS) { # add this condition
|
||||
return 403; # to make nginx return 403
|
||||
} # when the client has no valid certificate
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding the User Certificates to the client machine/browsers
|
||||
|
||||
Most browsers have a search feature on their setting page. Just search for certificates. It will show you the setting
|
||||
for certificate management.
|
||||
|
||||
### Google Chrome (and Chromium-based browsers)
|
||||
|
||||
Google Chrome and most Chromium-based (e.g., Vivaldi, Microsoft Edge) browsers installed on Windows or Mac rely on
|
||||
the operating system key management. So when you open the setting for certificate management, an external window
|
||||
will open.
|
||||
|
||||
<figure>
|
||||
<img alt="MacOS Keychain Access" src="https://assets.suyono.me/post/nginx-ssl-client-certificate-verification-manage-access-to-a-site/mac_keychain_1.png" width="740" height="483">
|
||||
<figcaption>MacOS Keychain Access Window, accessible from settings page</figcaption>
|
||||
</figure>
|
||||
|
||||
On Mac, it is called Keychain Access. To add a certificate, drag the pfx file onto Keychain Access. You'll need to
|
||||
input the exact export password when you convert the crt file to pfx/p12 format.
|
||||
|
||||
When you click Manage device certificate from the browser setting page, this window will open on Windows. You can import
|
||||
the pfx file using this window.
|
||||
|
||||
<figure>
|
||||
<img alt="Windows certificate dialog" src="https://assets.suyono.me/post/nginx-ssl-client-certificate-verification-manage-access-to-a-site/certificates.png" width="503" height="467">
|
||||
<figcaption>Certificates dialog on Widows, open from chrome settings page</figcaption>
|
||||
</figure>
|
||||
|
||||
Alternatively, you can use the certmgr to import the certificate. You can open it from the Windows setting or Control Panel.
|
||||
|
||||
<figure>
|
||||
<img alt="Windows certificates manager" src="https://assets.suyono.me/post/nginx-ssl-client-certificate-verification-manage-access-to-a-site/certmgr.png" width="626" height="444">
|
||||
<figcaption>Windows certificates manager, accessible from Control Panel</figcaption>
|
||||
</figure>
|
||||
|
||||
### Mozilla Firefox
|
||||
|
||||
Firefox has its own Certificate Manager dialog. You can import and manage the certificate from it. It also connects to
|
||||
the operating system certificate management.
|
||||
|
||||
<figure>
|
||||
<img alt="Firefox certificates manager" src="https://assets.suyono.me/post/nginx-ssl-client-certificate-verification-manage-access-to-a-site/firefox_certificate_manager_1.png" width="740" height="441">
|
||||
<figcaption>Mozilla Firefox Certificate Manager</figcaption>
|
||||
</figure>
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
You can use any browser or tool, like cURL, to test the client certificate verification setup. If your client
|
||||
certificate verification succeeds, you can open the page using a browser. If your browser shows something like
|
||||
403 Forbidden, it means either your browser does not have the certificate or something wrong in your setup.
|
||||
|
||||
### cURL
|
||||
|
||||
Without a valid client certificate
|
||||
|
||||
```sh
|
||||
curl -v https://www.example.com/
|
||||
```
|
||||
|
||||
response
|
||||
|
||||
```
|
||||
> GET / HTTP/2
|
||||
> Host: www.example.com
|
||||
> user-agent: curl/7.74.0
|
||||
> accept: */*
|
||||
>
|
||||
< HTTP/2 403
|
||||
< server: nginx
|
||||
< date: Wed, 12 Jul 2023 04:54:02 GMT
|
||||
< content-type: text/html
|
||||
< content-length: 146
|
||||
<
|
||||
<html>
|
||||
<head><title>403 Forbidden</title></head>
|
||||
<body>
|
||||
<center><h1>403 Forbidden</h1></center>
|
||||
<hr><center>nginx</center>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
With a valid client certificate
|
||||
|
||||
```sh
|
||||
curl --cert user.crt --key user.key -v https://www.example.com/
|
||||
```
|
||||
|
||||
response
|
||||
|
||||
```
|
||||
> GET / HTTP/2
|
||||
> Host: www.example.com
|
||||
> user-agent: curl/7.74.0
|
||||
> accept: */*
|
||||
>
|
||||
< HTTP/2 200
|
||||
< server: nginx
|
||||
.
|
||||
.
|
||||
.
|
||||
snipped
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Revoking Access
|
||||
|
||||
This setup recognizes users by the certificate they are using. Revoking access here means revoking the users'
|
||||
certificates. We can achieve this by leveraging OpenSSL's CRL feature. To use it, we need to have the CA database.
|
||||
I explained how to set it up in the section above.
|
||||
|
||||
### Revoke client certificate
|
||||
|
||||
```sh
|
||||
openssl ca -config ca.cnf -revoke user.crt
|
||||
```
|
||||
|
||||
### Generate CRL file
|
||||
|
||||
```sh
|
||||
openssl ca -config ca.cnf -gencrl -out crl.pem
|
||||
```
|
||||
|
||||
### Verifying CRL file
|
||||
|
||||
```sh
|
||||
openssl crl -in crl.pem -noout -text
|
||||
```
|
||||
|
||||
### Nginx configuration for CRL
|
||||
|
||||
You need to add the `ssl_crl` directive in the Nginx configuration file, as shown in the example below.
|
||||
|
||||
```nginx
|
||||
....
|
||||
ssl_client_certificate /path/to/client/verification/ca.crt;
|
||||
ssl_verify_client optional;
|
||||
ssl_verify_depth 2;
|
||||
ssl_crl /path/to/crl.pem; # configure nginx to read the crl file
|
||||
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
....
|
||||
```
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const 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
|
||||
|
||||
43
package.json
43
package.json
@@ -10,16 +10,35 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "20.6.5",
|
||||
"@types/react": "18.2.22",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"autoprefixer": "10.4.16",
|
||||
"eslint": "8.50.0",
|
||||
"eslint-config-next": "13.5.2",
|
||||
"next": "13.5.2",
|
||||
"postcss": "8.4.30",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tailwindcss": "3.3.3",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
"@types/react": "18.3.1",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"autoprefixer": "10.4.19",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"highlight.js": "^11.9.0",
|
||||
"lowlight": "^3.1.0",
|
||||
"mysql2": "^3.9.7",
|
||||
"next": "14.2.3",
|
||||
"postcss": "8.4.38",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-remark": "^2.1.0",
|
||||
"redis": "^4.6.13",
|
||||
"rehype-highlight": "^7.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-react": "^8.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"sharp": "^0.33.3",
|
||||
"tailwindcss": "3.4.3",
|
||||
"typescript": "5.2.2",
|
||||
"unified": "^11.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "3.0.3"
|
||||
},
|
||||
"packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab"
|
||||
}
|
||||
|
||||
7
pm2.config.js
Normal file
7
pm2.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
apps: [{
|
||||
script: "server.js",
|
||||
instances: 4,
|
||||
exec_mode: "cluster"
|
||||
}]
|
||||
}
|
||||
5951
pnpm-lock.yaml
generated
5951
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,12 @@ const config: Config = {
|
||||
'gradient-conic':
|
||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||
},
|
||||
width: {
|
||||
'224': '56rem',
|
||||
},
|
||||
height: {
|
||||
'192': '48rem',
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||
Reference in New Issue
Block a user