dh_demo

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

commit e3db10620f4077a9b55fb706179d97fda470248b
parent 3e54feb1fe9115d191961d4fed9e619a9ad80937
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Fri, 27 Jan 2023 11:12:12 +0900

feat: Redis 기반 Session 추가

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

Diffstat:
A.idea/jsLibraryMappings.xml | 7+++++++
M.pnp.cjs | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A.yarn/cache/@redis-bloom-npm-1.2.0-c3ffcb341a-8c21422728.zip | 0
A.yarn/cache/@redis-client-npm-1.5.2-18bbe90822-b23af6e82d.zip | 0
A.yarn/cache/@redis-graph-npm-1.1.0-84c3d1d722-d3df807108.zip | 0
A.yarn/cache/@redis-json-npm-1.0.4-213f95e10f-de07f9c37a.zip | 0
A.yarn/cache/@redis-search-npm-1.1.1-228edc852f-5c574679e8.zip | 0
A.yarn/cache/@redis-time-series-npm-1.0.4-1dfa6fdc7e-a5fca079de.zip | 0
A.yarn/cache/cluster-key-slot-npm-1.1.2-0571a28825-be0ad2d262.zip | 0
A.yarn/cache/generic-pool-npm-3.9.0-21fff1a77f-3d89e9b201.zip | 0
A.yarn/cache/redis-npm-4.6.1-69fb823c06-0fc9811f4e.zip | 0
M.yarn/install-state.gz | 0
Mlib/models/login_info.ts | 12++++++++----
Alib/models/user_acl_groups.ts | 43+++++++++++++++++++++++++++++++++++++++++++
Alib/redis.ts | 24++++++++++++++++++++++++
Mlib/security/acl.ts | 12++++++++++--
Alib/security/session.ts | 30++++++++++++++++++++++++++++++
Mlib/security/token.ts | 15++++-----------
Mpackage.json | 1+
Mpages/api/auth/refresh.ts | 20+++++++++++++++++---
Mpages/api/auth/token.ts | 45++++++++++++++++++++++++++++++++-------------
Msql/0001_base.sql | 5++++-
Myarn.lock | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
23 files changed, 429 insertions(+), 41 deletions(-)

diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="JavaScriptLibraryMappings"> + <includedPredefinedLibrary name="Node.js Core" /> + </component> +</project> +\ No newline at end of file diff --git a/.pnp.cjs b/.pnp.cjs @@ -56,6 +56,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["pino", "npm:8.8.0"],\ ["react", "npm:18.2.0"],\ ["react-dom", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:18.2.0"],\ + ["redis", "npm:4.6.1"],\ ["sass", "npm:1.57.1"],\ ["superjson", "npm:1.12.2"],\ ["typescript", "patch:typescript@npm%3A4.9.4#~builtin<compat/typescript>::version=4.9.4&hash=ad5954"],\ @@ -1495,6 +1496,128 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@redis/bloom", [\ + ["npm:1.2.0", {\ + "packageLocation": "./.yarn/cache/@redis-bloom-npm-1.2.0-c3ffcb341a-8c21422728.zip/node_modules/@redis/bloom/",\ + "packageDependencies": [\ + ["@redis/bloom", "npm:1.2.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.2.0", {\ + "packageLocation": "./.yarn/__virtual__/@redis-bloom-virtual-55d8dbbf85/0/cache/@redis-bloom-npm-1.2.0-c3ffcb341a-8c21422728.zip/node_modules/@redis/bloom/",\ + "packageDependencies": [\ + ["@redis/bloom", "virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.2.0"],\ + ["@redis/client", "npm:1.5.2"],\ + ["@types/redis__client", null]\ + ],\ + "packagePeers": [\ + "@redis/client",\ + "@types/redis__client"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@redis/client", [\ + ["npm:1.5.2", {\ + "packageLocation": "./.yarn/cache/@redis-client-npm-1.5.2-18bbe90822-b23af6e82d.zip/node_modules/@redis/client/",\ + "packageDependencies": [\ + ["@redis/client", "npm:1.5.2"],\ + ["cluster-key-slot", "npm:1.1.2"],\ + ["generic-pool", "npm:3.9.0"],\ + ["yallist", "npm:4.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@redis/graph", [\ + ["npm:1.1.0", {\ + "packageLocation": "./.yarn/cache/@redis-graph-npm-1.1.0-84c3d1d722-d3df807108.zip/node_modules/@redis/graph/",\ + "packageDependencies": [\ + ["@redis/graph", "npm:1.1.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.1.0", {\ + "packageLocation": "./.yarn/__virtual__/@redis-graph-virtual-405dec6f0e/0/cache/@redis-graph-npm-1.1.0-84c3d1d722-d3df807108.zip/node_modules/@redis/graph/",\ + "packageDependencies": [\ + ["@redis/graph", "virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.1.0"],\ + ["@redis/client", "npm:1.5.2"],\ + ["@types/redis__client", null]\ + ],\ + "packagePeers": [\ + "@redis/client",\ + "@types/redis__client"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@redis/json", [\ + ["npm:1.0.4", {\ + "packageLocation": "./.yarn/cache/@redis-json-npm-1.0.4-213f95e10f-de07f9c37a.zip/node_modules/@redis/json/",\ + "packageDependencies": [\ + ["@redis/json", "npm:1.0.4"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.0.4", {\ + "packageLocation": "./.yarn/__virtual__/@redis-json-virtual-47024e7e1a/0/cache/@redis-json-npm-1.0.4-213f95e10f-de07f9c37a.zip/node_modules/@redis/json/",\ + "packageDependencies": [\ + ["@redis/json", "virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.0.4"],\ + ["@redis/client", "npm:1.5.2"],\ + ["@types/redis__client", null]\ + ],\ + "packagePeers": [\ + "@redis/client",\ + "@types/redis__client"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@redis/search", [\ + ["npm:1.1.1", {\ + "packageLocation": "./.yarn/cache/@redis-search-npm-1.1.1-228edc852f-5c574679e8.zip/node_modules/@redis/search/",\ + "packageDependencies": [\ + ["@redis/search", "npm:1.1.1"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.1.1", {\ + "packageLocation": "./.yarn/__virtual__/@redis-search-virtual-0ac2f79fc0/0/cache/@redis-search-npm-1.1.1-228edc852f-5c574679e8.zip/node_modules/@redis/search/",\ + "packageDependencies": [\ + ["@redis/search", "virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.1.1"],\ + ["@redis/client", "npm:1.5.2"],\ + ["@types/redis__client", null]\ + ],\ + "packagePeers": [\ + "@redis/client",\ + "@types/redis__client"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@redis/time-series", [\ + ["npm:1.0.4", {\ + "packageLocation": "./.yarn/cache/@redis-time-series-npm-1.0.4-1dfa6fdc7e-a5fca079de.zip/node_modules/@redis/time-series/",\ + "packageDependencies": [\ + ["@redis/time-series", "npm:1.0.4"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.0.4", {\ + "packageLocation": "./.yarn/__virtual__/@redis-time-series-virtual-a598a1a087/0/cache/@redis-time-series-npm-1.0.4-1dfa6fdc7e-a5fca079de.zip/node_modules/@redis/time-series/",\ + "packageDependencies": [\ + ["@redis/time-series", "virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.0.4"],\ + ["@redis/client", "npm:1.5.2"],\ + ["@types/redis__client", null]\ + ],\ + "packagePeers": [\ + "@redis/client",\ + "@types/redis__client"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@rushstack/eslint-patch", [\ ["npm:1.2.0", {\ "packageLocation": "./.yarn/cache/@rushstack-eslint-patch-npm-1.2.0-917f402e4e-faa749faae.zip/node_modules/@rushstack/eslint-patch/",\ @@ -2789,6 +2912,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["cluster-key-slot", [\ + ["npm:1.1.2", {\ + "packageLocation": "./.yarn/cache/cluster-key-slot-npm-1.1.2-0571a28825-be0ad2d262.zip/node_modules/cluster-key-slot/",\ + "packageDependencies": [\ + ["cluster-key-slot", "npm:1.1.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["co", [\ ["npm:4.6.0", {\ "packageLocation": "./.yarn/cache/co-npm-4.6.0-03f2d1feb6-5210d92230.zip/node_modules/co/",\ @@ -3250,6 +3382,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["pino", "npm:8.8.0"],\ ["react", "npm:18.2.0"],\ ["react-dom", "virtual:185201ad25745f54898a2e9f78d456a655eb75b6cdedfba903ba6a6b91ecfe544fb067556e53d84d3f5dee5e27bf03679e7ed01de96d422947cdba9f3cf4c1cd#npm:18.2.0"],\ + ["redis", "npm:4.6.1"],\ ["sass", "npm:1.57.1"],\ ["superjson", "npm:1.12.2"],\ ["typescript", "patch:typescript@npm%3A4.9.4#~builtin<compat/typescript>::version=4.9.4&hash=ad5954"],\ @@ -4228,6 +4361,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["generic-pool", [\ + ["npm:3.9.0", {\ + "packageLocation": "./.yarn/cache/generic-pool-npm-3.9.0-21fff1a77f-3d89e9b201.zip/node_modules/generic-pool/",\ + "packageDependencies": [\ + ["generic-pool", "npm:3.9.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["gensync", [\ ["npm:1.0.0-beta.2", {\ "packageLocation": "./.yarn/cache/gensync-npm-1.0.0-beta.2-224666d72f-a7437e58c6.zip/node_modules/gensync/",\ @@ -7297,6 +7439,21 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["redis", [\ + ["npm:4.6.1", {\ + "packageLocation": "./.yarn/cache/redis-npm-4.6.1-69fb823c06-0fc9811f4e.zip/node_modules/redis/",\ + "packageDependencies": [\ + ["redis", "npm:4.6.1"],\ + ["@redis/bloom", "virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.2.0"],\ + ["@redis/client", "npm:1.5.2"],\ + ["@redis/graph", "virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.1.0"],\ + ["@redis/json", "virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.0.4"],\ + ["@redis/search", "virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.1.1"],\ + ["@redis/time-series", "virtual:69fb823c06ba220e04106d60001a379cfbc41d4af4686d144cafeec2b1ef7ae478d490b2f27c6d3898fad210fb40aca27c942d5185cb9a3a1af894397b6b1841#npm:1.0.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["regenerator-runtime", [\ ["npm:0.13.11", {\ "packageLocation": "./.yarn/cache/regenerator-runtime-npm-0.13.11-90bf536060-27481628d2.zip/node_modules/regenerator-runtime/",\ diff --git a/.yarn/cache/@redis-bloom-npm-1.2.0-c3ffcb341a-8c21422728.zip b/.yarn/cache/@redis-bloom-npm-1.2.0-c3ffcb341a-8c21422728.zip Binary files differ. diff --git a/.yarn/cache/@redis-client-npm-1.5.2-18bbe90822-b23af6e82d.zip b/.yarn/cache/@redis-client-npm-1.5.2-18bbe90822-b23af6e82d.zip Binary files differ. diff --git a/.yarn/cache/@redis-graph-npm-1.1.0-84c3d1d722-d3df807108.zip b/.yarn/cache/@redis-graph-npm-1.1.0-84c3d1d722-d3df807108.zip Binary files differ. diff --git a/.yarn/cache/@redis-json-npm-1.0.4-213f95e10f-de07f9c37a.zip b/.yarn/cache/@redis-json-npm-1.0.4-213f95e10f-de07f9c37a.zip Binary files differ. diff --git a/.yarn/cache/@redis-search-npm-1.1.1-228edc852f-5c574679e8.zip b/.yarn/cache/@redis-search-npm-1.1.1-228edc852f-5c574679e8.zip Binary files differ. diff --git a/.yarn/cache/@redis-time-series-npm-1.0.4-1dfa6fdc7e-a5fca079de.zip b/.yarn/cache/@redis-time-series-npm-1.0.4-1dfa6fdc7e-a5fca079de.zip Binary files differ. diff --git a/.yarn/cache/cluster-key-slot-npm-1.1.2-0571a28825-be0ad2d262.zip b/.yarn/cache/cluster-key-slot-npm-1.1.2-0571a28825-be0ad2d262.zip Binary files differ. diff --git a/.yarn/cache/generic-pool-npm-3.9.0-21fff1a77f-3d89e9b201.zip b/.yarn/cache/generic-pool-npm-3.9.0-21fff1a77f-3d89e9b201.zip Binary files differ. diff --git a/.yarn/cache/redis-npm-4.6.1-69fb823c06-0fc9811f4e.zip b/.yarn/cache/redis-npm-4.6.1-69fb823c06-0fc9811f4e.zip Binary files differ. diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz Binary files differ. diff --git a/lib/models/login_info.ts b/lib/models/login_info.ts @@ -1,4 +1,5 @@ import db from '@/lib/db' +import { modelBehaviour } from '@/lib/model_helpers' import { OkPacket, RowDataPacket } from 'mysql2' export interface LoginInfo { @@ -40,10 +41,13 @@ const SQL_GET_LOGIN_INFO_VIA_EMAIL = ` LIMIT 1 ` -export async function getLoginInfoViaEmail (email: string): Promise<LoginInfo | null> { - const [rows] = await db.query<RowDataPacket[]>({ +export const getLoginInfoViaEmail = modelBehaviour< + [email: string], + LoginInfo | null +>(async (conn, args) => { + const [rows] = await conn.query<RowDataPacket[]>({ sql: SQL_GET_LOGIN_INFO_VIA_EMAIL, - }, [email]) + }, args) if (rows.length === 0) { return null @@ -57,7 +61,7 @@ export async function getLoginInfoViaEmail (email: string): Promise<LoginInfo | createdAt: row[3], updatedAt: row[4], } -} +}) const SQL_CREATE_LOGIN_INFO = ` INSERT INTO logins (email, password_hash) diff --git a/lib/models/user_acl_groups.ts b/lib/models/user_acl_groups.ts @@ -0,0 +1,43 @@ +import { modelBehaviour } from '@/lib/model_helpers' +import { UserProfile } from '@/lib/models/user_profile' +import { RowDataPacket } from 'mysql2' + +const SQL_GET_USER_ACL_GROUPS = ` + SELECT acl_group + FROM user_acl_groups + WHERE login_id = ? +` + +export const getUserACLGroups = modelBehaviour< + [loginId: number], + string[] +>(async (conn, args) => { + const [rows] = await conn.query<RowDataPacket[]>({ + sql: SQL_GET_USER_ACL_GROUPS, + }, args) + + return rows.map(row => row[0]) +}) + +const SQL_GET_ACL_GROUP_USERS = ` + SELECT p.login_id, p.nickname, p.bio, p.updated_at + FROM user_acl_groups g + inner join user_profiles p on g.login_id = p.login_id + WHERE acl_group = ? +` + +export const getACLGroupUsers = modelBehaviour< + [aclGroup: string], + UserProfile[] +>(async (conn, args) => { + const [rows] = await conn.query<RowDataPacket[]>({ + sql: SQL_GET_ACL_GROUP_USERS, + }, args) + + return rows.map(row => ({ + loginId: row[0], + nickname: row[1], + bio: row[2], + updatedAt: row[3], + })) +}) diff --git a/lib/redis.ts b/lib/redis.ts @@ -0,0 +1,24 @@ +import { parseIntOrDefault } from '@/lib/utils/number' +import { createClient, RedisClientType } from 'redis' + +const REDIS_CLIENT_NAME = 'wiki' + +let client: RedisClientType | null + +export async function getRedis () { + if (client != null) { + return client + } + + client = createClient({ + url: process.env.WIKI_REDIS_URL, + username: process.env.WIKI_REDIS_USERNAME, + password: process.env.WIKI_REDIS_PASSWORD, + name: REDIS_CLIENT_NAME, + database: parseIntOrDefault(process.env.WIKI_REDIS_DATABASE, 0), + }) + + await client.connect() + return client +} + diff --git a/lib/security/acl.ts b/lib/security/acl.ts @@ -1,4 +1,5 @@ import { AccessTokenPayload } from '@/lib/security/token' +import { Connection } from 'mysql2/promise' export interface ACLItem { cond: string @@ -17,7 +18,11 @@ export const ACL_ACTION_MANAGE = 'manage' export const ACL_ACTION_TALK = 'talk' export const ACL_ACTION_CREATE_THREAD = 'create_thread' -export function resolveACL (token: AccessTokenPayload | null, acl: ACL | null | undefined, action: string): boolean { +export function resolveACL ( + token: AccessTokenPayload | null, + acl: ACL | null | undefined, + action: string +): boolean { const items = acl?.[action] if (items == null || items.length === 0) { return true @@ -31,7 +36,10 @@ export function resolveACL (token: AccessTokenPayload | null, acl: ACL | null | return allow } -function resolveACLItem (token: AccessTokenPayload | null, item: ACLItem): boolean | null { +function resolveACLItem ( + token: AccessTokenPayload | null, + item: ACLItem +): boolean | null { const [prefix, suffix] = item.cond.split(':') switch (prefix) { diff --git a/lib/security/session.ts b/lib/security/session.ts @@ -0,0 +1,30 @@ +import { getRedis } from '@/lib/redis' + +export interface Session { + id: string // `tid` in token + uid: number + aclGroups?: string[] +} + +export function sessionKey (sid: string) { + return `sess:${sid}` +} + +export async function putSession (sess: Session) { + const redis = await getRedis() + await redis.set(sessionKey(sess.id), JSON.stringify(sess)) +} + +export async function getSession (sid: string) { + const redis = await getRedis() + const data = await redis.get(sessionKey(sid)) + if (data == null) { + return null + } + return JSON.parse(data) as Session +} + +export async function deleteSession (sid: string) { + const redis = await getRedis() + await redis.del(sessionKey(sid)) +} diff --git a/lib/security/token.ts b/lib/security/token.ts @@ -1,6 +1,5 @@ import { getAccessTokenCookieName } from '@/lib/env' import { sign, verify, JwtPayload } from 'jsonwebtoken' -import { nanoid } from 'nanoid' import { NextApiRequest } from 'next' const TOKEN_ALGORITHM = 'HS256' @@ -10,7 +9,6 @@ export const REFRESH_TOKEN_EXPIRES_IN = 30 * 24 * 60 * 60 // 30일 export interface AccessTokenPayload extends JwtPayload { tid?: string uid?: number - aclGroup?: string[] } export function getTokenSecret () { @@ -22,15 +20,11 @@ export function getTokenSecret () { * @returns [accessToken, refreshToken, tokenId] */ export function signToken ( + tid: string, uid: number, -): [string, string, string] { - const tokenId = nanoid() - +): [string, string] { const accessToken = sign( - { - tid: tokenId, - uid: uid, - }, + { tid, uid }, getTokenSecret(), { algorithm: TOKEN_ALGORITHM, @@ -40,7 +34,7 @@ export function signToken ( const refreshToken = sign( { - tid: tokenId, + tid: tid, uid: uid, refresh: true, }, @@ -54,7 +48,6 @@ export function signToken ( return [ accessToken, refreshToken, - tokenId, ] } diff --git a/package.json b/package.json @@ -29,6 +29,7 @@ "pino": "^8.8.0", "react": "18.2.0", "react-dom": "18.2.0", + "redis": "^4.6.1", "superjson": "^1.12.2", "typescript": "4.9.4", "yaml": "^2.2.1" diff --git a/pages/api/auth/refresh.ts b/pages/api/auth/refresh.ts @@ -1,7 +1,9 @@ import { ERR_METHOD_NOT_ALLOWED, ERR_UNAUTHORIZED } from '@/lib/apierror' import { getRefreshTokenCookieName } from '@/lib/env' +import { getSession, putSession } from '@/lib/security/session' import { verifyToken } from '@/lib/security/token' import { signAndSendToken } from '@/pages/api/auth/token' +import { nanoid } from 'nanoid' import { NextApiRequest, NextApiResponse } from 'next' export default async function handler ( @@ -25,9 +27,21 @@ export default async function handler ( return } - const { uid } = token as { tid: string, uid: number } + const { tid: oldTID, uid } = token as { tid: string, uid: number } - // TODO: Revoke old token + // 기존 세션이 없으면 유효하지 않은 세션 + const session = await getSession(oldTID) + if (session == null) { + res.status(401).json(ERR_UNAUTHORIZED) + return + } + + // Update session - 기존 TID를 새로운 TID로 교체해, 기존 토큰을 무효화 + const tid = nanoid() + await putSession({ + ...session, + id: tid, + }) - signAndSendToken(res, uid) + await signAndSendToken(res, tid, uid) } diff --git a/pages/api/auth/token.ts b/pages/api/auth/token.ts @@ -1,8 +1,12 @@ import { ApiError } from '@/lib/apierror' import { getAccessTokenCookieName, getRefreshTokenCookieName } from '@/lib/env' +import { withConnection } from '@/lib/model_helpers' import { getLoginInfoViaEmail } from '@/lib/models/login_info' +import { getUserACLGroups } from '@/lib/models/user_acl_groups' import { comparePasswordHash } from '@/lib/security/password' +import { putSession } from '@/lib/security/session' import { REFRESH_TOKEN_EXPIRES_IN, signToken, TOKEN_EXPIRES_IN } from '@/lib/security/token' +import { nanoid } from 'nanoid' import type { NextApiRequest, NextApiResponse } from 'next' export interface TokenRequest { @@ -34,26 +38,41 @@ export default async function handler ( // 사용자 정보를 가져옴 const { email, password } = req.body as TokenRequest - const loginInfo = await getLoginInfoViaEmail(email) - if (loginInfo == null) { - res.status(401).json(ERR_INVALID_CREDENTIALS) - return - } + await withConnection(async (conn) => { + const loginInfo = await getLoginInfoViaEmail(conn, [email]) + if (loginInfo == null) { + res.status(401).json(ERR_INVALID_CREDENTIALS) + return + } - if (!await comparePasswordHash(password, loginInfo.passwordHash)) { - res.status(401).json(ERR_INVALID_CREDENTIALS) - return - } + if (!await comparePasswordHash(password, loginInfo.passwordHash)) { + res.status(401).json(ERR_INVALID_CREDENTIALS) + return + } - // JWT 토큰 발급 - signAndSendToken(res, loginInfo.id) + const tid = nanoid() + const aclGroups = await getUserACLGroups(conn, [loginInfo.id]) + + await putSession({ + id: tid, + uid: loginInfo.id, + aclGroups: aclGroups, + }) + + // JWT 토큰 발급 + await signAndSendToken(res, tid, loginInfo.id) + }) } -export function signAndSendToken ( +/** + * 토큰에 서명하고 클라이언트에 보냅니다. + */ +export async function signAndSendToken ( res: NextApiResponse<TokenResponse>, + tid: string, uid: number, ) { - const [accessToken, refreshToken] = signToken(uid) + const [accessToken, refreshToken] = signToken(tid, uid) res.setHeader('Set-Cookie', [ `${getAccessTokenCookieName()}=${accessToken}; Path=/; SameSite=Strict; Max-Age=${TOKEN_EXPIRES_IN}`, diff --git a/sql/0001_base.sql b/sql/0001_base.sql @@ -31,7 +31,10 @@ create table user_acl_groups login_id int not null references logins (id) on delete cascade on update cascade, acl_group varchar(255) not null, - primary key (login_id, acl_group) + + primary key (login_id, acl_group), + index (login_id), + index (acl_group) ); create table wikis diff --git a/yarn.lock b/yarn.lock @@ -970,6 +970,62 @@ __metadata: languageName: node linkType: hard +"@redis/bloom@npm:1.2.0": + version: 1.2.0 + resolution: "@redis/bloom@npm:1.2.0" + peerDependencies: + "@redis/client": ^1.0.0 + checksum: 8c214227287d6b278109098bca00afc601cf84f7da9c6c24f4fa7d3854b946170e5893aa86ed607ba017a4198231d570541c79931b98b6d50b262971022d1d6c + languageName: node + linkType: hard + +"@redis/client@npm:1.5.2": + version: 1.5.2 + resolution: "@redis/client@npm:1.5.2" + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + checksum: b23af6e82da02d6e079218c5e39ce35900d4001581d893446a9138e9d4a0a6c8348d34eec083743e602ec3ce9419bfbd06fe68497f93b97424d45093850da25f + languageName: node + linkType: hard + +"@redis/graph@npm:1.1.0": + version: 1.1.0 + resolution: "@redis/graph@npm:1.1.0" + peerDependencies: + "@redis/client": ^1.0.0 + checksum: d3df807108a42929ed65269c691fe6ab7eda55de91318f02a22b2d637c1bfef8817fccd17025904f5a0be8cf1cea5941334ec9f10719336da5d8f1c54cd4997e + languageName: node + linkType: hard + +"@redis/json@npm:1.0.4": + version: 1.0.4 + resolution: "@redis/json@npm:1.0.4" + peerDependencies: + "@redis/client": ^1.0.0 + checksum: de07f9c37abed603dec352593eb69fc8a94475e7f86b4f65b9805394492d448a1e4181db74269d80eb9dba6f3ae8a41804204821db36bb801cd7c1e30ac7ec80 + languageName: node + linkType: hard + +"@redis/search@npm:1.1.1": + version: 1.1.1 + resolution: "@redis/search@npm:1.1.1" + peerDependencies: + "@redis/client": ^1.0.0 + checksum: 5c574679e803530d0b1a704eb7c22292849e0a36dfc9a84a5359051f9da6e0dab72e11ade5654a69d9984bb45d2f7cdf901111135247670824013145cf28d326 + languageName: node + linkType: hard + +"@redis/time-series@npm:1.0.4": + version: 1.0.4 + resolution: "@redis/time-series@npm:1.0.4" + peerDependencies: + "@redis/client": ^1.0.0 + checksum: a5fca079deb04a2f204a7f9a375a6ff698a119d5dd53f7581fa8fd9e3bacacf1ecb0253b97fada484a012fea7a98014bc0f4f79707d4e92ff61c00318f2bfe04 + languageName: node + linkType: hard + "@rushstack/eslint-patch@npm:^1.1.3": version: 1.2.0 resolution: "@rushstack/eslint-patch@npm:1.2.0" @@ -2020,6 +2076,13 @@ __metadata: languageName: node linkType: hard +"cluster-key-slot@npm:1.1.2": + version: 1.1.2 + resolution: "cluster-key-slot@npm:1.1.2" + checksum: be0ad2d262502adc998597e83f9ded1b80f827f0452127c5a37b22dfca36bab8edf393f7b25bb626006fb9fb2436106939ede6d2d6ecf4229b96a47f27edd681 + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -2402,6 +2465,7 @@ __metadata: pino: ^8.8.0 react: 18.2.0 react-dom: 18.2.0 + redis: ^4.6.1 sass: ^1.57.1 superjson: ^1.12.2 typescript: 4.9.4 @@ -3205,6 +3269,13 @@ __metadata: languageName: node linkType: hard +"generic-pool@npm:3.9.0": + version: 3.9.0 + resolution: "generic-pool@npm:3.9.0" + checksum: 3d89e9b2018d2e3bbf44fec78c76b2b7d56d6a484237aa9daf6ff6eedb14b0899dadd703b5d810219baab2eb28e5128fb18b29e91e602deb2eccac14492d8ca8 + languageName: node + linkType: hard + "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -5866,6 +5937,20 @@ __metadata: languageName: node linkType: hard +"redis@npm:^4.6.1": + version: 4.6.1 + resolution: "redis@npm:4.6.1" + dependencies: + "@redis/bloom": 1.2.0 + "@redis/client": 1.5.2 + "@redis/graph": 1.1.0 + "@redis/json": 1.0.4 + "@redis/search": 1.1.1 + "@redis/time-series": 1.0.4 + checksum: 0fc9811f4ebe32ff15085f10323db578da433ad90a6e8d209262b20f5a5d1402418c12773f8650eab5a7a0efd7128dd911e8700a56fea4ec0fcba618dd048e89 + languageName: node + linkType: hard + "regenerator-runtime@npm:^0.13.11": version: 0.13.11 resolution: "regenerator-runtime@npm:0.13.11" @@ -6990,6 +7075,13 @@ __metadata: languageName: node linkType: hard +"yallist@npm:4.0.0, yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 + languageName: node + linkType: hard + "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" @@ -6997,13 +7089,6 @@ __metadata: languageName: node linkType: hard -"yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 - languageName: node - linkType: hard - "yaml@npm:^2.2.1": version: 2.2.1 resolution: "yaml@npm:2.2.1"