dh_demo

DreamHanks demo project
git clone git://git.lair.cx/dh_demo
Log | Files | Refs | README

commit fd3426866dda07bcb1d1854f28aefba5b71cee8a
parent eb4d4dcc449faf0a6e79af02b74a458952893b03
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Thu, 19 Jan 2023 14:24:42 +0900

lint: 코드 정리

Signed-off-by: Yongbin Kim <iam@yongbin.kim>

Diffstat:
Mcomponents/contexts/TokenContext.tsx | 9+++++----
Mcomponents/elements/Logo.tsx | 14++++++++++----
Mcomponents/layout/Header.tsx | 4++--
Mlib/apierror.ts | 2+-
Mlib/classnames.ts | 4++--
Dlib/email.ts | 20--------------------
Alib/email/index.ts | 20++++++++++++++++++++
Alib/email/templates.ts | 30++++++++++++++++++++++++++++++
Dlib/email_templates.ts | 30------------------------------
Mlib/env.ts | 4++--
Mlib/hooks/use_api.ts | 2+-
Mlib/models/login_info.ts | 24++++++++++++------------
Mlib/models/user_profile.ts | 8++++----
Mlib/security/token.ts | 2+-
Mpages/_document.tsx | 8++++----
Mpages/api/auth/refresh.ts | 4++--
Mpages/api/auth/token.ts | 2+-
Mpages/api/users/signup/[id].ts | 1-
Mpages/api/users/signup/index.ts | 4++--
Dpages/index.tsx | 12------------
Mpages/users/login.tsx | 4++--
Mpages/users/signup/index.tsx | 2+-
Msql/0001_base.sql | 31++++++++++++++++++++++++++++++-
23 files changed, 132 insertions(+), 109 deletions(-)

diff --git a/components/contexts/TokenContext.tsx b/components/contexts/TokenContext.tsx @@ -12,7 +12,8 @@ export interface AccessTokenPayload extends JwtPayload { } const TokenContext = createContext<AccessTokenPayload | null>(null) -const TokenRefreshContext = createContext<() => void>(() => {}) +const TokenRefreshContext = createContext<() => void>(() => { +}) export function TokenProvider ({ children }: { children: ReactNode }) { const [accessToken, setAccessToken] = useState<AccessTokenPayload | null>(null) @@ -40,7 +41,7 @@ export function TokenProvider ({ children }: { children: ReactNode }) { console.log('TokenContext: refresh token') fetch('/api/auth/refresh', { - method: 'POST' + method: 'POST', }).then(res => { // 갱신하지 못했을 때에는 무시 if (res.status !== 200) { @@ -66,7 +67,7 @@ export function TokenProvider ({ children }: { children: ReactNode }) { const trigger = useCallback( () => setCheckCounter(v => v + 1), - [] + [], ) return ( @@ -94,7 +95,7 @@ function getCookie (name: string) { name = name.toLowerCase() let cookie = document.cookie - for (;;) { + for (; ;) { let caps = regexCookie.exec(cookie) if (caps == null) { return null diff --git a/components/elements/Logo.tsx b/components/elements/Logo.tsx @@ -1,11 +1,17 @@ import styles from './Logo.module.css' -export default function Logo() { +export default function Logo () { return ( <svg className={styles.logo} width="56" height="24"> - <path d="M16.609-3.467h4.578c.906 0 1.582.038 2.027.114.445.075.844.233 1.195.472.352.24.645.559.879.957.235.399.352.846.352 1.34a2.67 2.67 0 0 1-.434 1.477 2.69 2.69 0 0 1-1.175 1.008c.698.203 1.234.549 1.609 1.039.375.489.562 1.065.562 1.726 0 .521-.121 1.028-.363 1.52a3.069 3.069 0 0 1-.992 1.18c-.419.294-.936.475-1.551.542-.385.042-1.315.068-2.789.079h-3.898V-3.467Zm2.312 1.907v2.648h1.516c.901 0 1.461-.013 1.68-.039.395-.047.707-.183.933-.41.227-.227.34-.525.34-.895 0-.354-.098-.641-.293-.863-.195-.221-.486-.355-.871-.402-.229-.026-.888-.039-1.977-.039h-1.328Zm0 4.554v3.063h2.141c.833 0 1.362-.024 1.586-.07.344-.063.623-.215.84-.457.216-.243.324-.567.324-.973 0-.344-.083-.635-.25-.875a1.418 1.418 0 0 0-.723-.524c-.315-.109-.999-.164-2.051-.164h-1.867ZM28.163 7.987V-3.467h4.868c1.224 0 2.113.103 2.668.309a2.64 2.64 0 0 1 1.332 1.098c.333.526.5 1.127.5 1.804 0 .86-.253 1.569-.758 2.129s-1.261.913-2.266 1.059c.5.292.913.612 1.238.961.326.349.765.969 1.317 1.859l1.398 2.235h-2.765l-1.672-2.493c-.594-.89-1-1.451-1.219-1.683a1.774 1.774 0 0 0-.695-.477c-.245-.086-.633-.129-1.164-.129h-.469v4.782h-2.313Zm2.313-6.61h1.711c1.109 0 1.802-.047 2.078-.14.276-.094.492-.256.648-.485.157-.229.235-.515.235-.859 0-.386-.103-.697-.309-.934-.206-.237-.496-.386-.871-.449-.187-.026-.75-.039-1.687-.039h-1.805v2.906Z" transform="translate(-16.511 9.74)"/> - <path d="M50.038-3.467h-2.515l-1 2.602h-4.578l-.946-2.602h-2.453l4.461 11.454h2.445l4.586-11.454Zm-4.257 4.532-1.579 4.25-1.546-4.25h3.125Z" transform="matrix(1 0 0 1 -16.511 9.74)"/> - <path d="M51.288 7.987V-3.467h2.25l4.688 7.649v-7.649h2.148V7.987h-2.32L53.437.518v7.469h-2.149ZM62.812-3.467h4.226c.954 0 1.68.073 2.18.219.672.198 1.247.55 1.727 1.055.479.505.843 1.124 1.093 1.855.25.732.375 1.635.375 2.707 0 .943-.117 1.756-.351 2.438-.287.833-.695 1.508-1.227 2.023-.401.391-.942.696-1.625.914-.51.162-1.192.243-2.047.243h-4.351V-3.467Zm2.312 1.938v7.586h1.727c.646 0 1.112-.037 1.398-.109.375-.094.687-.253.934-.477.247-.224.449-.592.605-1.105.157-.513.235-1.213.235-2.098 0-.886-.078-1.565-.235-2.039-.156-.474-.375-.844-.656-1.11a2.207 2.207 0 0 0-1.07-.539c-.323-.073-.956-.109-1.899-.109h-1.039Z" transform="translate(-16.511 9.74)"/> + <path + d="M16.609-3.467h4.578c.906 0 1.582.038 2.027.114.445.075.844.233 1.195.472.352.24.645.559.879.957.235.399.352.846.352 1.34a2.67 2.67 0 0 1-.434 1.477 2.69 2.69 0 0 1-1.175 1.008c.698.203 1.234.549 1.609 1.039.375.489.562 1.065.562 1.726 0 .521-.121 1.028-.363 1.52a3.069 3.069 0 0 1-.992 1.18c-.419.294-.936.475-1.551.542-.385.042-1.315.068-2.789.079h-3.898V-3.467Zm2.312 1.907v2.648h1.516c.901 0 1.461-.013 1.68-.039.395-.047.707-.183.933-.41.227-.227.34-.525.34-.895 0-.354-.098-.641-.293-.863-.195-.221-.486-.355-.871-.402-.229-.026-.888-.039-1.977-.039h-1.328Zm0 4.554v3.063h2.141c.833 0 1.362-.024 1.586-.07.344-.063.623-.215.84-.457.216-.243.324-.567.324-.973 0-.344-.083-.635-.25-.875a1.418 1.418 0 0 0-.723-.524c-.315-.109-.999-.164-2.051-.164h-1.867ZM28.163 7.987V-3.467h4.868c1.224 0 2.113.103 2.668.309a2.64 2.64 0 0 1 1.332 1.098c.333.526.5 1.127.5 1.804 0 .86-.253 1.569-.758 2.129s-1.261.913-2.266 1.059c.5.292.913.612 1.238.961.326.349.765.969 1.317 1.859l1.398 2.235h-2.765l-1.672-2.493c-.594-.89-1-1.451-1.219-1.683a1.774 1.774 0 0 0-.695-.477c-.245-.086-.633-.129-1.164-.129h-.469v4.782h-2.313Zm2.313-6.61h1.711c1.109 0 1.802-.047 2.078-.14.276-.094.492-.256.648-.485.157-.229.235-.515.235-.859 0-.386-.103-.697-.309-.934-.206-.237-.496-.386-.871-.449-.187-.026-.75-.039-1.687-.039h-1.805v2.906Z" + transform="translate(-16.511 9.74)"/> + <path + d="M50.038-3.467h-2.515l-1 2.602h-4.578l-.946-2.602h-2.453l4.461 11.454h2.445l4.586-11.454Zm-4.257 4.532-1.579 4.25-1.546-4.25h3.125Z" + transform="matrix(1 0 0 1 -16.511 9.74)"/> + <path + d="M51.288 7.987V-3.467h2.25l4.688 7.649v-7.649h2.148V7.987h-2.32L53.437.518v7.469h-2.149ZM62.812-3.467h4.226c.954 0 1.68.073 2.18.219.672.198 1.247.55 1.727 1.055.479.505.843 1.124 1.093 1.855.25.732.375 1.635.375 2.707 0 .943-.117 1.756-.351 2.438-.287.833-.695 1.508-1.227 2.023-.401.391-.942.696-1.625.914-.51.162-1.192.243-2.047.243h-4.351V-3.467Zm2.312 1.938v7.586h1.727c.646 0 1.112-.037 1.398-.109.375-.094.687-.253.934-.477.247-.224.449-.592.605-1.105.157-.513.235-1.213.235-2.098 0-.886-.078-1.565-.235-2.039-.156-.474-.375-.844-.656-1.11a2.207 2.207 0 0 0-1.07-.539c-.323-.073-.956-.109-1.899-.109h-1.039Z" + transform="translate(-16.511 9.74)"/> </svg> ) } diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx @@ -36,10 +36,10 @@ export default function Header () { </> ) : ( <> - <NavbarItem href={`/wiki/${userProfile.login_id}`}> + <NavbarItem href={`/wiki/`}> 내 위키 </NavbarItem> - <NavbarItem href={`/users/${userProfile.login_id}`}> + <NavbarItem href={`/users/${userProfile.loginId}`}> {userProfile.nickname} </NavbarItem> </> diff --git a/lib/apierror.ts b/lib/apierror.ts @@ -4,7 +4,7 @@ export interface ApiError { field?: string } -export function isApiError(obj: any): obj is ApiError { +export function isApiError (obj: any): obj is ApiError { return obj.code != null } diff --git a/lib/classnames.ts b/lib/classnames.ts @@ -1,10 +1,10 @@ -export default function classNames(...args: unknown[]): { className: string } { +export default function classNames (...args: unknown[]): { className: string } { return { className: args.reduce<string>((prev, curr) => { if (typeof curr !== 'string' || curr.length == 0) { return prev } return `${prev} ${curr}` - }, '') + }, '').substring(1), } } diff --git a/lib/email.ts b/lib/email.ts @@ -1,20 +0,0 @@ -import nodemailer from 'nodemailer' - -const transporter = nodemailer.createTransport({ - host: process.env.WIKI_SMTP_HOST ?? 'localhost', - port: Number(process.env.WIKI_SMTP_PORT) ?? 25, - secure: process.env.WIKI_SMTP_FORCE_TLS === 'true', - auth: { - user: process.env.WIKI_SMTP_USER ?? '', - pass: process.env.WIKI_SMTP_PASS ?? '', - } -}) - -export const sendEmail = async (to: string, subject: string, html: string) => { - return await transporter.sendMail({ - from: process.env.WIKI_SMTP_FROM, - to, - subject, - html - }) -} diff --git a/lib/email/index.ts b/lib/email/index.ts @@ -0,0 +1,20 @@ +import nodemailer from 'nodemailer' + +const transporter = nodemailer.createTransport({ + host: process.env.WIKI_SMTP_HOST ?? 'localhost', + port: Number(process.env.WIKI_SMTP_PORT) ?? 25, + secure: process.env.WIKI_SMTP_FORCE_TLS === 'true', + auth: { + user: process.env.WIKI_SMTP_USER ?? '', + pass: process.env.WIKI_SMTP_PASS ?? '', + }, +}) + +export const sendEmail = async (to: string, subject: string, html: string) => { + return await transporter.sendMail({ + from: process.env.WIKI_SMTP_FROM, + to, + subject, + html, + }) +} diff --git a/lib/email/templates.ts b/lib/email/templates.ts @@ -0,0 +1,30 @@ +import Handlebars from 'handlebars' + +export const emailTemplates = { + signupConfirmation: Handlebars.compile(` + <html> + <body> + <div style="max-width:500px;width:100%;margin:0 auto;padding:1rem;font-family:sans-serif;background-color:#fff;margin-top:40px;margin-bottom:60px;border-radius:16px;box-shadow:0px 1px 2px 0px #00000033, 0px 2px 6px 2px #0000001A"> + <h1> + 계정 만들기 + </h1> + <p> + 누군가 이 메일 주소로 회원가입을 요청했습니다.<br/> + 본인이 하신 요청이 아니라면 이 메일을 무시하셔도 괜찮습니다. + </p> + <p> + 계속하려면 아래 링크로 이동해주세요.<br/> + <a href="{{signupURL}}">{{signupURL}}</a> + </p> + <p> + 감사합니다. + </p> + </div> + </body> + </html> + `), +} + +export function getSignupConfirmationEmailHTML (signupURL: string): string { + return emailTemplates.signupConfirmation({ signupURL }) +} diff --git a/lib/email_templates.ts b/lib/email_templates.ts @@ -1,30 +0,0 @@ -import Handlebars from 'handlebars' - -export const emailTemplates = { - signupConfirmation: Handlebars.compile(` - <html> - <body> - <div style="max-width:500px;width:100%;margin:0 auto;padding:1rem;font-family:sans-serif;background-color:#fff;margin-top:40px;margin-bottom:60px;border-radius:16px;box-shadow:0px 1px 2px 0px #00000033, 0px 2px 6px 2px #0000001A"> - <h1> - 계정 만들기 - </h1> - <p> - 누군가 이 메일 주소로 회원가입을 요청했습니다.<br/> - 본인이 하신 요청이 아니라면 이 메일을 무시하셔도 괜찮습니다. - </p> - <p> - 계속하려면 아래 링크로 이동해주세요.<br/> - <a href="{{signupURL}}">{{signupURL}}</a> - </p> - <p> - 감사합니다. - </p> - </div> - </body> - </html> - `), -} - -export function getSignupConfirmationEmailHTML(signupURL: string): string { - return emailTemplates.signupConfirmation({ signupURL }) -} diff --git a/lib/env.ts b/lib/env.ts @@ -1,7 +1,7 @@ -export function getAccessTokenCookieName() { +export function getAccessTokenCookieName () { return `${process.env.WIKI_JWT_COOKIE_PREFIX ?? 'wiki_jwt_'}access_token` } -export function getRefreshTokenCookieName() { +export function getRefreshTokenCookieName () { return `${process.env.WIKI_JWT_COOKIE_PREFIX ?? 'wiki_jwt_'}refresh_token` } diff --git a/lib/hooks/use_api.ts b/lib/hooks/use_api.ts @@ -1,4 +1,4 @@ -import { DependencyList, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useState } from 'react' export interface UseApiOptions<TBody> extends Omit<RequestInit, 'body'> { body?: (() => TBody) | TBody diff --git a/lib/models/login_info.ts b/lib/models/login_info.ts @@ -10,10 +10,10 @@ export interface LoginInfo { } const SQL_GET_LOGIN_INFO = ` - SELECT id, email, password_hash, created_at, updated_at - FROM logins - WHERE id = ? - LIMIT 1 + SELECT id, email, password_hash, created_at, updated_at + FROM logins + WHERE id = ? + LIMIT 1 ` export async function getLoginInfo (id: number): Promise<LoginInfo | null> { @@ -34,13 +34,13 @@ export async function getLoginInfo (id: number): Promise<LoginInfo | null> { } const SQL_GET_LOGIN_INFO_VIA_EMAIL = ` - SELECT id, email, password_hash, created_at, updated_at - FROM logins - WHERE email = ? - LIMIT 1 + SELECT id, email, password_hash, created_at, updated_at + FROM logins + WHERE email = ? + LIMIT 1 ` -export async function getLoginInfoViaEmail(email: string): Promise<LoginInfo | null> { +export async function getLoginInfoViaEmail (email: string): Promise<LoginInfo | null> { const [rows] = await db.query<RowDataPacket[]>({ sql: SQL_GET_LOGIN_INFO_VIA_EMAIL, }, [email]) @@ -60,11 +60,11 @@ export async function getLoginInfoViaEmail(email: string): Promise<LoginInfo | n } const SQL_CREATE_LOGIN_INFO = ` - INSERT INTO logins (email, password_hash) - VALUES (?, ?) + INSERT INTO logins (email, password_hash) + VALUES (?, ?) ` -export async function createLoginInfo(email: string, passwordHash: string): Promise<number> { +export async function createLoginInfo (email: string, passwordHash: string): Promise<number> { const [result] = await db.query<OkPacket>({ sql: SQL_CREATE_LOGIN_INFO, }, [email, passwordHash]) diff --git a/lib/models/user_profile.ts b/lib/models/user_profile.ts @@ -2,10 +2,10 @@ import db from '@/lib/db' import { OkPacket, RowDataPacket } from 'mysql2' export interface UserProfile { - login_id: number + loginId: number nickname: string bio: string - updated_at: Date + updatedAt: Date } const SQL_CREATE_USER_PROFILE = ` @@ -42,9 +42,9 @@ export async function getUserProfile (loginId: number): Promise<UserProfile | nu const row = rows[0] return { - login_id: row[0], + loginId: row[0], nickname: row[1], bio: row[2], - updated_at: row[3], + updatedAt: row[3], } } diff --git a/lib/security/token.ts b/lib/security/token.ts @@ -14,7 +14,7 @@ export function getTokenSecret () { * @returns [accessToken, refreshToken, tokenId] */ export function signToken ( - uid: number + uid: number, ): [string, string, string] { const tokenId = nanoid() diff --git a/pages/_document.tsx b/pages/_document.tsx @@ -1,13 +1,13 @@ import Header from '@/components/layout/Header' import { Html, Head, Main, NextScript } from 'next/document' -export default function Document() { +export default function Document () { return ( <Html lang="en"> - <Head /> + <Head/> <body> - <Main /> - <NextScript /> + <Main/> + <NextScript/> </body> </Html> ) diff --git a/pages/api/auth/refresh.ts b/pages/api/auth/refresh.ts @@ -4,7 +4,7 @@ import { verifyToken } from '@/lib/security/token' import { signAndSendToken } from '@/pages/api/auth/token' import { NextApiRequest, NextApiResponse } from 'next' -export default async function handler( +export default async function handler ( req: NextApiRequest, res: NextApiResponse, ) { @@ -25,7 +25,7 @@ export default async function handler( return } - const { tid, uid } = token as { tid: string, uid: number } + const { uid } = token as { tid: string, uid: number } // TODO: Revoke old token diff --git a/pages/api/auth/token.ts b/pages/api/auth/token.ts @@ -53,7 +53,7 @@ export function signAndSendToken ( res: NextApiResponse<TokenResponse>, uid: number, ) { - const [accessToken, refreshToken, tokenId] = signToken(uid) + const [accessToken, refreshToken] = signToken(uid) res.setHeader('Set-Cookie', [ `${getAccessTokenCookieName()}=${accessToken}; Path=/; SameSite=Strict; Max-Age=${TOKEN_EXPIRES_IN}`, diff --git a/pages/api/users/signup/[id].ts b/pages/api/users/signup/[id].ts @@ -7,7 +7,6 @@ import { import { createUserProfile } from '@/lib/models/user_profile' import bcrypt from 'bcrypt' import { NextApiRequest, NextApiResponse } from 'next' -import { useRouter } from 'next/router' interface SignupRequest { password: string diff --git a/pages/api/users/signup/index.ts b/pages/api/users/signup/index.ts @@ -1,6 +1,6 @@ import { ApiError, ERR_METHOD_NOT_ALLOWED } from '@/lib/apierror' import { sendEmail } from '@/lib/email' -import { getSignupConfirmationEmailHTML } from '@/lib/email_templates' +import { getSignupConfirmationEmailHTML } from '@/lib/email/templates' import { createSignupRequest } from '@/lib/models/signup_request' import { NextApiRequest, NextApiResponse } from 'next' @@ -32,7 +32,7 @@ export default async function handler ( res.status(200).json({ status: 'ok' }) } -function getIPAddress(req: NextApiRequest): string { +function getIPAddress (req: NextApiRequest): string { const result = req.headers['x-forwarded-for'] ?? req.socket.remoteAddress if (result == null) { return '' diff --git a/pages/index.tsx b/pages/index.tsx @@ -1,12 +0,0 @@ -import Container from '@/components/layout/Container' -import Section from '@/components/layout/Section' - -export default function Home () { - return ( - <Container> - <Section> - <p>Hello, World!</p> - </Section> - </Container> - ) -} diff --git a/pages/users/login.tsx b/pages/users/login.tsx @@ -20,7 +20,7 @@ export default function LoginPage () { <Section> <Container> - <LoginForm /> + <LoginForm/> </Container> </Section> </> @@ -38,7 +38,7 @@ function LoginForm () { { method: 'POST', credentials: 'same-origin', - body: () => ({ email, password }) + body: () => ({ email, password }), }, ) diff --git a/pages/users/signup/index.tsx b/pages/users/signup/index.tsx @@ -34,7 +34,7 @@ function SignUpForm () { '/api/users/signup', { method: 'POST', - body: () => ({ email }) + body: () => ({ email }), }, ) diff --git a/sql/0001_base.sql b/sql/0001_base.sql @@ -17,7 +17,6 @@ create table signup_requests confirmed_at datetime null ); -drop table if exists user_profiles; create table user_profiles ( login_id int not null primary key references logins (id) @@ -26,3 +25,33 @@ create table user_profiles bio text null, updated_at datetime null on update current_timestamp ); + +create table wikis +( + id int not null auto_increment primary key, + owner_id int not null references logins (id) + on delete restrict on update cascade, + slug varchar(48) not null unique check (slug <> ''), + title varchar(255) not null, + description text null, + created_at datetime not null default current_timestamp, + updated_at datetime null on update current_timestamp +); + +create table wiki_pages +( + wiki_id int not null references wikis (id) + on delete cascade on update cascade, + slug varchar(255) not null check (slug <> ''), + title varchar(255) not null check ( title <> '' ), + html text not null, + created_at datetime not null default current_timestamp, + updated_at datetime null on update current_timestamp, + + primary key (wiki_id, slug) +); + +create table wiki_changes +( + +);