Skip to content

Commit 08aa216

Browse files
committed
add: auth Directive
1 parent 5201f43 commit 08aa216

File tree

36 files changed

+573
-223
lines changed

36 files changed

+573
-223
lines changed

apps/book-server/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
],
2525
"license": "MIT",
2626
"dependencies": {
27+
"@fastify/cookie": "^9.3.1",
2728
"@fastify/cors": "^8.3.0",
2829
"@fastify/mongodb": "^8.0.0",
2930
"@graphql-tools/graphql-file-loader": "^8.0.0",
3031
"@graphql-tools/load": "^8.0.0",
3132
"@graphql-tools/merge": "^9.0.0",
33+
"@graphql-tools/utils": "^10.1.3",
3234
"@packages/database": "workspace:*",
3335
"@packages/library": "workspace:*",
3436
"@prisma/client": "^5.11.0",
@@ -40,6 +42,7 @@
4042
"fs-extra": "^11.2.0",
4143
"graphql": "^16.7.1",
4244
"graphql-scalars": "^1.22.2",
45+
"graphql-tools": "^9.0.1",
4346
"ioredis": "^5.3.2",
4447
"mercurius": "^14.0.0",
4548
"mime": "^4.0.1",

apps/book-server/scripts/createMock.mts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MongoService } from '@lib/mongo/MongoService.mjs'
33
import { getMockPages } from 'test/mock/mockBook.mjs'
44
import { Writer } from '@packages/database/velog-book-mongo'
55
import { faker } from '@faker-js/faker'
6+
import { urlAlphabet, random, customRandom } from 'nanoid'
67

78
class Seeder {
89
constructor(private readonly mongo: MongoService) {}
@@ -28,11 +29,20 @@ class Seeder {
2829
writer_id: writer.id,
2930
...page,
3031
}))
32+
33+
const title = 'Learning bunJS is Fun!'
34+
const urlSlug = `${title}-${customRandom(urlAlphabet, 10, random)().toLocaleLowerCase()}`
35+
3136
const book = await this.mongo.book.create({
3237
data: {
3338
writer_id: writer.id,
34-
title: 'Learning bunJS is Fun!',
39+
title: title,
3540
thumbnail: faker.image.dataUri(),
41+
description: faker.lorem.paragraph(2),
42+
is_temp: false,
43+
is_private: false,
44+
is_published: true,
45+
url_slug: urlSlug,
3646
pages: {
3747
createMany: {
3848
data: mockPages,

apps/book-server/src/app.mts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1+
import cookie from '@fastify/cookie'
12
import Fastify from 'fastify'
23
import corsPlugin from '@plugins/global/corsPlugin.mjs'
34
import ipaddrPlugin from '@plugins/global/ipaddrPlugin.mjs'
45
import mercuriusPlugin from '@plugins/global/mercuriusPlugin.mjs'
6+
import authPlugin from '@plugins/global/authPlugin.mjs'
7+
import { ENV } from '@env'
58

69
const app = Fastify({
710
logger: true,
811
trustProxy: true,
912
})
1013

14+
app.register(cookie, { secret: ENV.cookieSecretKey })
1115
app.register(corsPlugin)
1216
app.register(ipaddrPlugin)
17+
app.register(authPlugin)
1318
app.register(mercuriusPlugin)
1419

1520
export default app
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const OK = 200
2+
const CREATED = 201
3+
const BAD_REQUEST = 400
4+
const UNAUTHORIZED = 401
5+
const FORBIDDEN = 403
6+
const NOT_FOUND = 404
7+
const CONFLICT = 409
8+
const INTERNAL_SERVER_ERROR = 500
9+
10+
export const HttpStatus = {
11+
OK,
12+
CREATED,
13+
BAD_REQUEST,
14+
FORBIDDEN,
15+
UNAUTHORIZED,
16+
NOT_FOUND,
17+
CONFLICT,
18+
INTERNAL_SERVER_ERROR,
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const OK = 'Ok'
2+
const CREATED = 'CREATED'
3+
const BAD_REQUEST = 'BAD_REQUEST'
4+
const UNAUTHORIZED = 'UNAUTHORIZED'
5+
const FORBIDDEN = 'FORBIDDEN'
6+
const NOT_FOUND = 'NOT_FOUND'
7+
const CONFLICT = 'CONFLICT'
8+
const INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR'
9+
10+
export const HttpStatusMessage = {
11+
OK,
12+
CREATED,
13+
BAD_REQUEST,
14+
FORBIDDEN,
15+
UNAUTHORIZED,
16+
NOT_FOUND,
17+
CONFLICT,
18+
INTERNAL_SERVER_ERROR,
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const ONE_MINUTE_IN_MS = 1000 * 60
2+
const ONE_HOUR_IN_MS = ONE_MINUTE_IN_MS * 60
3+
const ONE_DAY_IN_MS = ONE_HOUR_IN_MS * 24
4+
5+
const ONE_MINUTE_IN_S = 60
6+
const ONE_HOUR_IN_S = ONE_MINUTE_IN_S * 60
7+
const ONE_DAY_IN_S = ONE_HOUR_IN_S * 24
8+
9+
export const Time = {
10+
ONE_MINUTE_IN_MS,
11+
ONE_HOUR_IN_MS,
12+
ONE_DAY_IN_MS,
13+
14+
ONE_MINUTE_IN_S,
15+
ONE_HOUR_IN_S,
16+
ONE_DAY_IN_S,
17+
}

apps/book-server/src/common/interfaces/graphql.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export type GraphQLContextBase = {
99
request: FastifyRequest
1010
reply: FastifyReply
1111
ip: string | null
12+
writer: { id: string } | null
1213
}

apps/book-server/src/common/plugins/global/authPlugin.mts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,38 @@
11
import fp from 'fastify-plugin'
22
import { FastifyPluginAsync } from 'fastify'
3+
import { WriterService } from '@services/WriterService/index.mjs'
4+
import { container } from 'tsyringe'
5+
import { CookieService } from '@lib/cookie/CookieService.mjs'
6+
import { JwtService } from '@lib/jwt/JwtService.mjs'
7+
import type { AccessTokenData } from '@packages/library/jwt'
38

49
const authPlugin: FastifyPluginAsync = async (fastify) => {
5-
fastify.decorateRequest('user', null)
6-
fastify.addHook('preHandler', async (request) => {
10+
fastify.decorateRequest('writer', null)
11+
fastify.addHook('onRequest', async (request, reply) => {
712
if (request.url.includes('/auth/logout')) return
13+
14+
const cookieService = container.resolve(CookieService)
15+
try {
16+
const writerService = container.resolve(WriterService)
17+
const jwtService = container.resolve(JwtService)
18+
19+
let accessToken: string | undefined = request.cookies['access_token']
20+
const authorization = request.headers['authorization']
21+
22+
if (!accessToken && !!authorization && typeof authorization === 'string') {
23+
accessToken = authorization.split('Bearer ')[1]
24+
}
25+
26+
if (!accessToken) return
27+
const accessTokenData = await jwtService.decodeToken<AccessTokenData>(accessToken)
28+
const writerId = await writerService.checkExistsWriter(accessTokenData.user_id)
29+
if (!writerId) return
30+
request.writer = { id: writerId }
31+
} catch (error) {
32+
console.log('auth Plugin error', error)
33+
cookieService.clearCookie(reply, 'access_token')
34+
cookieService.clearCookie(reply, 'refresh_token')
35+
}
836
})
937
}
1038

apps/book-server/src/common/plugins/global/corsPlugin.mts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import fp from 'fastify-plugin'
2-
import { container } from 'tsyringe'
32
import { FastifyPluginAsync } from 'fastify'
43
import cors from '@fastify/cors'
54
import { ForbiddenError } from '@errors/ForbiddenError.mjs'
6-
import { EnvService } from '@lib/env/EnvService.mjs'
5+
import { ENV } from '@env'
76

87
const corsPlugin: FastifyPluginAsync = async (fastify) => {
98
const corsWhitelist: RegExp[] = [
@@ -14,8 +13,7 @@ const corsPlugin: FastifyPluginAsync = async (fastify) => {
1413
/https:\/\/velog.graphcdn.app/,
1514
]
1615

17-
const env = container.resolve(EnvService)
18-
if (env.get('appEnv') === 'development') {
16+
if (ENV.appEnv === 'development') {
1917
corsWhitelist.push(/^http:\/\/localhost/)
2018
}
2119

apps/book-server/src/common/plugins/global/mercuriusPlugin.mts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,39 @@ import type { FastifyPluginAsync } from 'fastify'
55
import { isHttpError } from '@errors/HttpError.mjs'
66
import { container } from 'tsyringe'
77
import { DiscordService } from '@lib/discord/DiscordService.mjs'
8-
import { EnvService } from '@lib/env/EnvService.mjs'
98
import { MqService } from '@lib/mq/MqService.mjs'
109
import { GraphQLContextBase } from '@interfaces/graphql.mjs'
10+
import { ENV } from '@env'
11+
import { schemaTransforms } from '@graphql/transformer/index.mjs'
1112

1213
const mercuriusPlugin: FastifyPluginAsync = async (fastify) => {
13-
const env = container.resolve(EnvService)
14-
container.register<MqService>(MqService, {
15-
useValue: new MqService({ host: env.get('redisHost'), port: 6379 }),
16-
})
1714
const mqService = container.resolve(MqService)
1815

1916
fastify.register(mercurius, {
2017
logLevel: 'error',
2118
schema,
2219
resolvers: resolvers,
23-
graphiql: env.get('appEnv') !== 'production',
20+
graphiql: ENV.appEnv !== 'production',
2421
subscription: {
2522
emitter: mqService.emitter,
2623
verifyClient: (info, next) => {
2724
// if (info.req.headers['x-fastify-header'] !== 'fastify is awesome !') {
2825
// return next(false) // the connection is not allowed
2926
// }
30-
next(true) // the connection is allowed
27+
28+
next(false)
29+
// next(true) // the connection is allowed
3130
},
3231
},
3332
context: (request, reply): GraphQLContextBase => {
3433
return {
3534
request,
3635
reply,
3736
ip: request.ipaddr,
37+
writer: request.writer,
3838
}
3939
},
40+
schemaTransforms,
4041
errorHandler: (error, request) => {
4142
const { name, message, code, stack, errors, statusCode } = error
4243
const result = {
@@ -50,7 +51,7 @@ const mercuriusPlugin: FastifyPluginAsync = async (fastify) => {
5051
message: error.message,
5152
})),
5253
}
53-
if (env.get('appEnv') === 'development') {
54+
if (ENV.appEnv === 'development') {
5455
console.log('errorHandler')
5556
request.log.error(request, 'errorHandler')
5657
} else {

apps/book-server/src/env.mts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import dotenv from 'dotenv'
2+
import { existsSync } from 'fs'
3+
import { dirname, join } from 'path'
4+
import { fileURLToPath } from 'url'
5+
import { z } from 'zod'
6+
7+
type DockerEnv = 'development' | 'stage' | 'production'
8+
type AppEnvironment = 'development' | 'production'
9+
type EnvFiles = Record<DockerEnv, string>
10+
11+
function resolveDir(dir: string): string {
12+
const __filename = fileURLToPath(import.meta.url)
13+
const splited = dirname(__filename).split('/src')
14+
const cwd = splited.slice(0, -1).join('/src')
15+
return join(cwd, dir)
16+
}
17+
18+
const envFiles: EnvFiles = {
19+
development: '.env.development',
20+
production: '.env.production',
21+
stage: '.env.stage',
22+
}
23+
24+
if (!process.env.DOCKER_ENV && process.env.NODE_ENV !== undefined) {
25+
console.error(
26+
'Development environment was initiated, despite the absence of the Docker environment.',
27+
)
28+
}
29+
30+
const dockerEnv = (process.env.DOCKER_ENV as DockerEnv) || 'development'
31+
const appEnv: AppEnvironment = ['stage', 'production'].includes(dockerEnv)
32+
? 'production'
33+
: 'development'
34+
35+
const envFile = envFiles[dockerEnv]
36+
const prefix = './env'
37+
38+
const configPath = resolveDir(`${prefix}/${envFile}`)
39+
40+
if (!existsSync(configPath)) {
41+
console.log(`Read target: ${configPath}`)
42+
throw new Error('Not found environment file')
43+
}
44+
45+
dotenv.config({ path: configPath })
46+
47+
const env = z.object({
48+
dockerEnv: z.enum(['development', 'production', 'stage']),
49+
appEnv: z.enum(['development', 'production']),
50+
port: z.number(),
51+
mongoUrl: z.string(),
52+
discordBotToken: z.string(),
53+
discordErrorChannel: z.string(),
54+
redisHost: z.string(),
55+
redisPort: z.number(),
56+
bookBucketName: z.string(),
57+
bookBucketUrl: z.string(),
58+
cookieSecretKey: z.string(),
59+
jwtSecretKey: z.string(),
60+
})
61+
62+
export const ENV = env.parse({
63+
dockerEnv,
64+
appEnv,
65+
port: Number(process.env.PORT),
66+
mongoUrl: process.env.MONGO_URL,
67+
discordBotToken: process.env.DISCORD_BOT_TOKEN,
68+
discordErrorChannel: process.env.DISCORD_ERROR_CHANNEL,
69+
redisHost: process.env.REDIS_HOST,
70+
redisPort: Number(process.env.REDIS_PORT),
71+
bookBucketName: process.env.BOOK_BUCKET_NAME,
72+
bookBucketUrl: process.env.BOOK_BUCKET_URL,
73+
cookieSecretKey: process.env.COOKIE_SECRET_KEY,
74+
jwtSecretKey: process.env.JWT_SECRET_KEY,
75+
})

apps/book-server/src/graphql/Base.gql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
directive @auth on FIELD_DEFINITION

apps/book-server/src/graphql/Book.gql

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ type Query {
1111
}
1212

1313
type Mutation {
14-
deploy(input: BookIDInput!): Void
15-
build(input: BookIDInput!): Void
14+
deploy(input: BookIDInput!): Void @auth
15+
build(input: BookIDInput!): Void @auth
1616
}
1717

1818
type Subscription {
19-
bookBuildInstalled(input: BookIDInput!): SubScriptionPayload
20-
bookBuildCompleted(input: BookIDInput!): SubScriptionPayload
21-
bookDeployCompleted(input: BookIDInput!): SubScriptionPayload
19+
bookBuildInstalled(input: BookIDInput!): SubScriptionPayload @auth
20+
bookBuildCompleted(input: BookIDInput!): SubScriptionPayload @auth
21+
bookDeployCompleted(input: BookIDInput!): SubScriptionPayload @auth
2222
}
2323

2424
input BookIDInput {

apps/book-server/src/graphql/index.mts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { container } from 'tsyringe'
21
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'
32
import { loadSchemaSync } from '@graphql-tools/load'
43
import { mergeResolvers } from '@graphql-tools/merge'
@@ -8,17 +7,15 @@ import { DateTimeISOResolver, VoidResolver, PositiveIntResolver } from 'graphql-
87
import { IResolvers, MercuriusContext } from 'mercurius'
98
import { basename, dirname, resolve } from 'path'
109
import { fileURLToPath } from 'url'
11-
import { EnvService } from '@lib/env/EnvService.mjs'
10+
import { ENV } from '@env'
1211

1312
async function resolverAutoLoader(): Promise<any[]> {
14-
const env = container.resolve(EnvService)
15-
1613
const __filename = fileURLToPath(import.meta.url)
1714
const __dirname = dirname(__filename)
1815
const resolverFolderPath = resolve(__dirname, 'resolvers')
1916

2017
const promises = readdirSync(resolverFolderPath).map(async (resolverPath) => {
21-
const suffix = env.get('appEnv') === 'development' ? '.mts' : '.mjs'
18+
const suffix = ENV.appEnv === 'development' ? '.mts' : '.mjs'
2219
const resolvername = basename(resolverPath, suffix)
2320
const resolver = await import(`./resolvers/${resolvername}.mjs`)
2421
return resolver.default

0 commit comments

Comments
 (0)