Skip to content

Commit 2084018

Browse files
committed
feat: structure / image upload
1 parent d8201c4 commit 2084018

File tree

48 files changed

+811
-103
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+811
-103
lines changed

.env.example

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ REDIS_TOKEN_EXPIRATION=86400
99
JWT_SECRET=
1010
JWT_EXPIRATION=24h
1111

12-
MAIL_HOST=sandbox.smtp.mailtrap.io
13-
MAIL_PORT=2525
12+
MAIL_HOST=127.0.0.1
13+
MAIL_PORT=1025
1414
MAIL_USER=
1515
MAIL_PASSWORD=
16-
MAIL_TPL_PATH=views/templates
16+
MAIL_TPL_PATH=templates
17+
18+
STORAGE_PATH=storage/public
1719

1820
API_LOG_FILENAME=api-logs.log

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ node_modules
22
public
33
.env
44
*.log
5-
.DS_Store
5+
.DS_Store
6+
storage/public

README.md

Lines changed: 40 additions & 33 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"watch": "npm run rm:public && cross-env ts-node-dev --files --respawn --inspect -r tsconfig-paths/register ./src",
2828
"build": "npm run rm:public && cross-env NODE_ENV=production tsc && tsc-alias && npm run copy:files",
2929
"rm:public": "rm -rf ./public",
30-
"copy:files": "cp .env ./public/.env && cp -R ./src/views ./public",
30+
"copy:files": "cp .env ./public/.env && cp -R ./src/templates ./public",
3131
"seed": "ts-node --files -r tsconfig-paths/register ./src/seeds",
3232
"lint": "eslint --debug ./src",
3333
"lint:fix": "eslint --debug ./src --fix",
@@ -42,9 +42,11 @@
4242
"@types/email-templates": "10.0.1",
4343
"@types/express": "4.17.14",
4444
"@types/jsonwebtoken": "8.5.9",
45+
"@types/multer": "1.4.7",
4546
"@types/node": "18.8.3",
4647
"@types/nodemailer": "6.4.7",
4748
"@types/randomstring": "1.1.8",
49+
"@types/sharp": "^0.31.1",
4850
"@types/validator": "13.7.11",
4951
"@typescript-eslint/eslint-plugin": "5.39.0",
5052
"@typescript-eslint/parser": "5.39.0",
@@ -73,9 +75,11 @@
7375
"i18next-http-middleware": "3.2.2",
7476
"jsonwebtoken": "8.5.1",
7577
"mongoose": "6.6.5",
78+
"multer": "1.4.5-lts.1",
7679
"nodemailer": "6.9.1",
7780
"randomstring": "1.2.3",
7881
"redis": "4.3.1",
82+
"sharp": "0.31.3",
7983
"validator": "13.7.0",
8084
"winston": "3.8.2"
8185
}

src/@types/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export declare global {
1919
MAIL_USER: string
2020
MAIL_PASSWORD: string
2121
MAIL_TPL_PATH: string
22+
STORAGE_PATH: string
2223
}
2324
}
2425
}

src/constants/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,12 @@ export enum ExpiresInDays {
22
Verification = 7,
33
ResetPassword = 7
44
}
5+
6+
export enum Mimetype {
7+
Jpeg = 'image/jpeg',
8+
Png = 'image/png'
9+
}
10+
11+
export enum ImageSizeInMB {
12+
Ten = 10
13+
}

src/contracts/jwt.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { ObjectId } from 'mongoose'
2+
13
export interface JwtUser {
2-
id: string
4+
id: ObjectId
35
}
46

57
export interface AccessToken {

src/contracts/media.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Model, ObjectId } from 'mongoose'
2+
3+
export interface IMedia {
4+
originalname: string
5+
encoding: string
6+
mimetype: string
7+
destination: string
8+
filename: string
9+
path: string
10+
size: number
11+
orderColumn: number
12+
refType: string
13+
refId: ObjectId
14+
user: ObjectId
15+
}
16+
17+
export type MediaFileUploadRequest = Omit<
18+
IMedia,
19+
'refId' | 'refType' | 'orderColumn'
20+
>
21+
22+
export type MediaModel = Model<IMedia>

src/contracts/request.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Request } from 'express'
2+
import { ObjectId } from 'mongoose'
23

34
export interface ContextRequest<T> extends Omit<Request, 'context'> {
45
context: T
@@ -22,7 +23,7 @@ export interface CombinedRequest<
2223

2324
export interface UserRequest {
2425
user: {
25-
id: string
26+
id: ObjectId
2627
email: string
2728
verified: boolean
2829
}

src/contracts/user.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
import { Model, ObjectId } from 'mongoose'
22

33
export interface IVerification {
4-
userId: string
54
email: string
65
accessToken: string
76
expiresIn: Date
7+
user: ObjectId
88
}
99

1010
export interface IResetPassword {
11-
userId: string
1211
accessToken: string
1312
expiresIn: Date
13+
user: ObjectId
1414
}
1515

1616
export interface IUser {
17+
id: ObjectId
1718
email: string
1819
password: string
1920
firstName?: string
2021
lastName?: string
2122
verified: boolean
2223
verifications: ObjectId[]
2324
resetPasswords: ObjectId[]
25+
media: ObjectId[]
2426
}
2527

2628
export interface IUserMethods {

src/controllers/authController.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const authController = {
101101
session
102102
)
103103

104-
await userService.pushVerificationsIdByUserId(
104+
await userService.addVerificationToUser(
105105
{
106106
userId: user.id,
107107
verificationId: verification.id
@@ -150,7 +150,7 @@ export const authController = {
150150
res: Response
151151
) => {
152152
try {
153-
await redis.client.set(`expiredToken:${accessToken}`, user.id, {
153+
await redis.client.set(`expiredToken:${accessToken}`, `${user.id}`, {
154154
EX: process.env.REDIS_TOKEN_EXPIRATION,
155155
NX: true
156156
})
@@ -198,7 +198,7 @@ export const authController = {
198198
session
199199
)
200200

201-
await userService.pushResetPasswordIdByUserId(
201+
await userService.addResetPasswordToUser(
202202
{
203203
userId: user.id,
204204
resetPasswordId: resetPassword.id
@@ -254,7 +254,7 @@ export const authController = {
254254
})
255255
}
256256

257-
const user = await userService.getById(resetPassword.userId)
257+
const user = await userService.getById(resetPassword.user)
258258

259259
if (!user) {
260260
return res.status(StatusCodes.NOT_FOUND).json({
@@ -267,7 +267,7 @@ export const authController = {
267267
const hashedPassword = await createHash(password)
268268

269269
await userService.updatePasswordByUserId(
270-
resetPassword.userId,
270+
resetPassword.user,
271271
hashedPassword,
272272
session
273273
)

src/controllers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { authController } from './authController'
22
export { userController } from './userController'
3+
export { mediaController } from './mediaController'

src/controllers/mediaController.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Response } from 'express'
2+
import { ReasonPhrases, StatusCodes } from 'http-status-codes'
3+
import winston from 'winston'
4+
5+
import { mediaService, userService } from '@/services'
6+
import { Image } from '@/infrastructure/image'
7+
import { ContextRequest, UserRequest } from '@/contracts/request'
8+
import { startSession } from 'mongoose'
9+
10+
export const mediaController = {
11+
create: async (
12+
{ context: { user }, file }: ContextRequest<UserRequest>,
13+
res: Response
14+
) => {
15+
const session = await startSession()
16+
17+
try {
18+
session.startTransaction()
19+
const media = await mediaService.create(
20+
{
21+
...(file as Express.Multer.File),
22+
user: user.id
23+
},
24+
session
25+
)
26+
27+
await userService.addMediaToUser({ userId: user.id, mediaId: media.id })
28+
29+
const image = await new Image(media).sharp()
30+
31+
await session.commitTransaction()
32+
session.endSession()
33+
34+
return res.status(StatusCodes.OK).json({
35+
data: { id: media.id, image },
36+
message: ReasonPhrases.OK,
37+
status: StatusCodes.OK
38+
})
39+
} catch (error) {
40+
winston.error(error)
41+
42+
await new Image(file as Express.Multer.File).deleteFile()
43+
44+
if (session.inTransaction()) {
45+
await session.abortTransaction()
46+
session.endSession()
47+
}
48+
49+
return res.status(StatusCodes.BAD_REQUEST).json({
50+
message: ReasonPhrases.BAD_REQUEST,
51+
status: StatusCodes.BAD_REQUEST
52+
})
53+
}
54+
}
55+
}

src/controllers/userController.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export const userController = {
9191
session
9292
)
9393

94-
await userService.pushVerificationsIdByUserId(
94+
await userService.addVerificationToUser(
9595
{
9696
userId: user.id,
9797
verificationId: verification.id
@@ -149,14 +149,14 @@ export const userController = {
149149
session.startTransaction()
150150

151151
await userService.updateVerificationAndEmailByUserId(
152-
verification.userId,
152+
verification.user,
153153
verification.email,
154154
session
155155
)
156156

157-
await verificationService.deleteManyByUserId(verification.userId, session)
157+
await verificationService.deleteManyByUserId(verification.user, session)
158158

159-
const { accessToken } = jwtSign(verification.userId)
159+
const { accessToken } = jwtSign(verification.user)
160160

161161
const userMail = new UserMail()
162162

@@ -285,7 +285,7 @@ export const userController = {
285285
)
286286
}
287287

288-
await userService.pushVerificationsIdByUserId(
288+
await userService.addVerificationToUser(
289289
{
290290
userId: user.id,
291291
verificationId: verification.id

src/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import express, { Express } from 'express'
2+
import { join } from 'path'
23
import 'dotenv/config'
34

4-
import '@/logger'
5+
import '@/infrastructure/logger'
56
import { mongoose, redis } from '@/dataSources'
67
import {
78
corsMiddleware,
@@ -17,8 +18,13 @@ redis.run()
1718
const app: Express = express()
1819

1920
app.use(
20-
express.json(),
21-
express.urlencoded({ extended: true }),
21+
join('/', process.env.STORAGE_PATH),
22+
express.static(join(__dirname, process.env.STORAGE_PATH))
23+
)
24+
25+
app.use(
26+
express.json({ limit: '10mb' }),
27+
express.urlencoded({ limit: '10mb', extended: true }),
2228
corsMiddleware,
2329
i18nextHttpMiddleware.handle(i18next),
2430
authMiddleware,

0 commit comments

Comments
 (0)