Skip to content

Commit 67efb89

Browse files
committed
Merge branch 'feature/handle-block-list' into feature/post-api
2 parents 2e646ae + 05cb5ed commit 67efb89

File tree

19 files changed

+183
-24
lines changed

19 files changed

+183
-24
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- CreateTable
2+
CREATE TABLE "dynamic_config_items" (
3+
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
4+
"value" VARCHAR(50) NOT NULL,
5+
"type" VARCHAR(50) NOT NULL,
6+
7+
CONSTRAINT "dynamic_config_items_pkey" PRIMARY KEY ("id")
8+
);
9+
10+
-- CreateIndex
11+
CREATE INDEX "dynamic_config_items_value_idx" ON "dynamic_config_items"("value");
12+
13+
-- CreateIndex
14+
CREATE UNIQUE INDEX "dynamic_config_items_value_type_key" ON "dynamic_config_items"("value", "type");

packages/velog-prisma/prisma/schema.prisma

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,3 +640,13 @@ model Notification {
640640
@@index([fk_user_id])
641641
@@map("notifications")
642642
}
643+
644+
model DynamicConfigItem {
645+
id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
646+
value String @db.VarChar(50)
647+
type String @db.VarChar(50)
648+
649+
@@unique([value, type])
650+
@@index([value])
651+
@@map("dynamic_config_items")
652+
}

packages/velog-scripts/env/.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ DATABASE_URL=
22
DISCORD_BOT_TOKEN=
33
DISCORD_PRIVATE_POSTS_CHANNEL_ID=
44
REDIS_HOST=
5-
SPAM_ACCOUNT_DISPLAY_NAME=
5+
SPAM_ACCOUNT_DISPLAY_NAME=
6+
BANNED_KEYWORDS=

packages/velog-scripts/env/env.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const env = z.object({
1515
discordPrivatePostsChannelId: z.string(),
1616
redisHost: z.string(),
1717
restorePostsUsername: z.string().optional(),
18+
bannedKeywords: z.array(z.string()),
1819
})
1920

2021
export const ENV = env.parse({
@@ -24,4 +25,5 @@ export const ENV = env.parse({
2425
discordPrivatePostsChannelId: process.env.DISCORD_PRIVATE_POSTS_CHANNEL_ID,
2526
redisHost: process.env.REDIS_HOST,
2627
restorePostsUsername: process.env.RESTORE_POSTS_USERNAME,
28+
bannedKeywords: process.env.BANNED_KEYWORDS?.split(','),
2729
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { DbService } from '../db/DbService.mjs'
2+
import { injectable, singleton } from 'tsyringe'
3+
4+
interface Service {
5+
addBlockList: (username: string) => Promise<void>
6+
readBlockList: () => Promise<string[]>
7+
}
8+
9+
@injectable()
10+
@singleton()
11+
export class BlockListService implements Service {
12+
constructor(private readonly db: DbService) {}
13+
public async addBlockList(username: string) {
14+
await this.db.dynamicConfigItem.create({
15+
data: {
16+
value: username,
17+
type: 'username',
18+
},
19+
})
20+
}
21+
public async readBlockList() {
22+
const blockList = await this.db.dynamicConfigItem.findMany({
23+
where: {
24+
type: 'username',
25+
},
26+
})
27+
return blockList.map((item) => item.value)
28+
}
29+
}

packages/velog-scripts/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"private:posts": "pnpm env:copy -e development && ts-node ./scripts/privatePostsOfSpamAccount.mts",
1111
"check:gql": "pnpm prisma:copy && pnpm env:copy -e development && ts-node ./scripts/checkGql.mts",
1212
"restore:posts": "pnpm env:copy -e development && pnpm prisma:copy && ts-node ./scripts/restorePosts.mts",
13+
"add:bannedKeyword": "pnpm env:copy -e development && pnpm prisma:copy && ts-node ./scripts/addBannedKeywords.mts",
1314
"env:copy": "tsx ./scripts/copyEnv.ts",
1415
"prisma:copy": "tsx ./scripts/copyPrisma.ts",
1516
"build": "tsc",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import 'reflect-metadata'
2+
import { ENV } from '../env/env.mjs'
3+
import { DbService } from '../lib/db/DbService.mjs'
4+
import { container, injectable, singleton } from 'tsyringe'
5+
6+
@singleton()
7+
@injectable()
8+
class Runner {
9+
constructor(private readonly db: DbService) {}
10+
public async run(bannedKeywords: string[]) {
11+
const data = bannedKeywords.map((value) => ({
12+
value,
13+
type: 'banned',
14+
}))
15+
16+
try {
17+
await this.db.dynamicConfigItem.createMany({
18+
data,
19+
})
20+
} catch (error) {
21+
console.log(error)
22+
}
23+
}
24+
}
25+
26+
;(async function () {
27+
const bannedKeywords = ENV.bannedKeywords
28+
if (!bannedKeywords) {
29+
throw new Error('No banned keywords')
30+
}
31+
32+
const runner = container.resolve(Runner)
33+
runner.run(bannedKeywords)
34+
})()

packages/velog-scripts/scripts/checkGql.mts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,5 +244,13 @@ type TargetInfo = {
244244
type Target = Record<string, TargetInfo>
245245

246246
function whiteList() {
247-
return ['logout', 'unregisterToken', 'acceptIntegration']
247+
return [
248+
'isLogged',
249+
'logout',
250+
'unregisterToken',
251+
'acceptIntegration',
252+
'readAllNotifications',
253+
'removeAllNotifications',
254+
'updateNotNoticeNotification',
255+
]
248256
}

packages/velog-scripts/scripts/privatePostsOfSpamAccount.mts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,16 @@ import { container, injectable } from 'tsyringe'
55
import inquirer from 'inquirer'
66
import { ENV } from '../env/env.mjs'
77
import { DiscordService } from '../lib/discord/DiscordService.mjs'
8-
import { RedisService } from '../lib/redis/RedisService.mjs'
8+
import { BlockListService } from '../lib/blockList/BlockListService.mjs'
99

1010
interface IRunner {}
1111

1212
@injectable()
1313
class Runner implements IRunner {
14-
blockList!: string[]
1514
constructor(
1615
private readonly db: DbService,
1716
private readonly discord: DiscordService,
18-
private readonly redis: RedisService,
17+
private readonly blockList: BlockListService,
1918
) {}
2019
public async run(names: string[]) {
2120
await this.init()
@@ -27,7 +26,7 @@ class Runner implements IRunner {
2726
const posts = await this.findWritenPostsByUserId(user.id)
2827

2928
// add block list
30-
await this.redis.addBlockList(username)
29+
await this.blockList.addBlockList(username)
3130

3231
if (posts.length === 0) {
3332
console.log(`${user.username} 유저의 비공개 처리 할 게시글이 없습니다.`)
@@ -88,9 +87,7 @@ class Runner implements IRunner {
8887
private async init() {
8988
try {
9089
await this.discord.connection()
91-
await this.redis.connection()
92-
93-
this.blockList = await this.redis.readBlockList()
90+
await this.db.$connect()
9491
} catch (error) {
9592
throw error
9693
}
@@ -133,7 +130,8 @@ class Runner implements IRunner {
133130
username: string,
134131
displayName: string | null,
135132
): Promise<AskDeletePostsResult> {
136-
if (this.blockList.includes(username)) {
133+
const blockedList = await this.blockList.readBlockList()
134+
if (blockedList.includes(username)) {
137135
return {
138136
posts,
139137
is_set_private: true,

packages/velog-server/src/graphql/resolvers/userResolvers.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ const userResolvers: Resolvers = {
8484
checkEmailExists: async (_, { input }) => {
8585
const userService = container.resolve(UserService)
8686
const user = await userService.findByEmail(input.email)
87-
console.log('user', user)
8887
return !!user
8988
},
9089
},

packages/velog-server/src/services/UserService/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,13 @@ export class UserService implements Service {
253253
try {
254254
if (ENV.dockerEnv === 'development') {
255255
console.log(`Login URL: ${ENV.clientV3Host}/email-change?code=${code}`)
256+
} else {
257+
await this.mail.sendMail({
258+
to: email,
259+
260+
...template,
261+
})
256262
}
257-
258-
await this.mail.sendMail({
259-
to: email,
260-
261-
...template,
262-
})
263263
} catch (error) {
264264
console.error('change email error', error)
265265
throw error

packages/velog-server/src/template/changeEmailTemplate.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ export const changeEmailTemplate: ChangeEmailTemplateArgs = (username, email, co
99
<a href="https://velog.io">
1010
<img src="https://images.velog.io/email-logo.png" style="display: block; width: 128px; margin: 0 auto;"/>
1111
</a>
12-
<div style="max-width: 100%; width: 400px; margin: 0 auto; padding: 1rem; text-align: justify; background: #f8f9fa; border: 1px solid #dee2e6; box-sizing: border-box; border-radius: 4px; color: #868e96; margin-top: 0.5rem; box-sizing: border-box;">
13-
<b style="black">안녕하세요!</b>
14-
${username}의 이메일을 ${email}${text}하는 것을 승인하겠습니까?
12+
<div style="max-width: 100%; margin: 0 auto; padding: 1rem; text-align: justify; background: #f8f9fa; border: 1px solid #dee2e6; box-sizing: border-box; border-radius: 4px; color: #868e96; margin-top: 0.5rem; box-sizing: border-box;">
13+
<div><b style="black">안녕하세요!</b></div>
14+
<p style="word-wrap: break-word; word-break: break-all;">${username}의 이메일을 ${email}${text}하는 것을 승인하겠습니까?</p>
1515
</div>
1616
<a href="${endpoint}/email-change?code=${code}" style="text-decoration: none; width: 400px; text-align:center; display:block; margin: 0 auto; margin-top: 1rem; background: #845ef7; padding-top: 1rem; color: white; font-size: 1.25rem; padding-bottom: 1rem; font-weight: 600; border-radius: 4px;">
1717
${text}하기
@@ -25,6 +25,7 @@ export const changeEmailTemplate: ChangeEmailTemplateArgs = (username, email, co
2525
</div>
2626
</div>
2727
`
28+
2829
return {
2930
subject,
3031
body,

packages/velog-web/src/app/notifications/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1+
import RequireLogin from '@/components/RequireLogin'
12
import NotificationList from '@/features/notification/components/NotificationList'
23
import NotificationSelector from '@/features/notification/components/NotificationSelector'
34
import NotificationTitle from '@/features/notification/components/NotificationTitle'
45
import getCurrentUser from '@/prefetch/getCurrentUser'
5-
import { notFound } from 'next/navigation'
66

77
export default async function NotificationPage() {
88
const user = await getCurrentUser()
99

1010
if (!user) {
11-
notFound()
11+
return <RequireLogin />
1212
}
1313

1414
return (

packages/velog-web/src/app/setting/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import RequireLogin from '@/components/RequireLogin'
12
import SettingEmailRow from '@/features/setting/components/SettingEmailRow'
23
import SettingEmailRulesRow from '@/features/setting/components/SettingEmailRulesRow'
34
import SettingSocialInfoRow from '@/features/setting/components/SettingSocialInfoRow'
@@ -13,7 +14,7 @@ export default async function SettingPage() {
1314
const user = await getCurrentUser()
1415

1516
if (!user) {
16-
notFound()
17+
return <RequireLogin />
1718
}
1819

1920
const velogConfig = await getVelogConfig({ username: user.username })

packages/velog-web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.module.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
display: flex;
33
width: 100%;
44
height: 100%;
5-
min-height: 100svh;
65
align-items: center;
76
justify-content: center;
87
flex-direction: column;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.block {
2+
height: 70svh;
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import RequireLogin from './RequireLogin'
2+
import { render } from '@testing-library/react'
3+
4+
describe('RequireLogin', () => {
5+
it('renders successfully', () => {
6+
render(<RequireLogin />)
7+
})
8+
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use client'
2+
3+
import styles from './RequireLogin.module.css'
4+
import { bindClassNames } from '@/lib/styles/bindClassNames'
5+
import ErrorScreenTemplate from '@/components/Error/ErrorScreenTemplate'
6+
import { UndrawLogin } from '@/assets/vectors/components'
7+
import { useModal } from '@/state/modal'
8+
import { useEffect } from 'react'
9+
import { useAuth } from '@/state/auth'
10+
import { usePathname, useRouter } from 'next/navigation'
11+
12+
const cx = bindClassNames(styles)
13+
14+
type Props = {
15+
redirectTo?: string
16+
}
17+
18+
function RequireLogin({ redirectTo }: Props) {
19+
const pathname = usePathname()
20+
const router = useRouter()
21+
const { actions } = useModal()
22+
23+
const redirectToPath = redirectTo || pathname
24+
25+
const {
26+
value: { currentUser },
27+
} = useAuth()
28+
29+
const onButtonClick = () => {
30+
actions.showModal('login', redirectToPath)
31+
}
32+
33+
useEffect(() => {
34+
if (!currentUser) return
35+
router.push(redirectToPath)
36+
}, [router, currentUser, redirectToPath])
37+
38+
return (
39+
<div className={cx('block')}>
40+
<ErrorScreenTemplate
41+
Illustration={UndrawLogin}
42+
message="로그인 후 이용해주세요."
43+
onButtonClick={onButtonClick}
44+
buttonText="로그인"
45+
/>
46+
</div>
47+
)
48+
}
49+
50+
export default RequireLogin
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './RequireLogin'

0 commit comments

Comments
 (0)