From f0bc3cf68c7a5021746ca39c8b4359633f0563c5 Mon Sep 17 00:00:00 2001 From: Vardan Mkhitaryan Date: Mon, 23 Dec 2024 11:52:04 +0400 Subject: [PATCH 1/5] Feat: user logo --- src/api/user/index.ts | 216 +++++++++++++++++- src/api/user/user.crud.ts | 34 +++ src/api/user/validation.schema.ts | 14 ++ .../20241218150018-add-logo-to-user.js | 17 ++ src/database/models/User.ts | 4 + 5 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 src/api/user/user.crud.ts create mode 100644 src/api/user/validation.schema.ts create mode 100644 src/database/migrations/20241218150018-add-logo-to-user.js diff --git a/src/api/user/index.ts b/src/api/user/index.ts index 8eed6d4..f2911a1 100644 --- a/src/api/user/index.ts +++ b/src/api/user/index.ts @@ -1,8 +1,10 @@ import express, { NextFunction, Request, Response } from 'express'; import { ErrorMessages } from '~/core/dictionary/error.messages'; -import { UnauthorizedError } from '~/core/errors'; +import { UnauthorizedError, UnprocessableEntityError } from '~/core/errors'; import { isAuthenticated } from '~/shared/user'; +import { UpdateUserLogoSchema } from './validation.schema'; +import { UserCrud } from './user.crud'; const route = express.Router(); @@ -50,6 +52,25 @@ const route = express.Router(); * name: * type: string * example: "John Doe" + * logo: + * type: string + * example: "Base64" + * 422: + * description: Unprocessable Entity - Invalid base64 + * content: + * application/json: + * schema: + * type: object + * properties: + * type: + * type: string + * example: UNPROCESSABLE_ENTITY_ERROR + * statusCode: + * type: integer + * example: 422 + * message: + * type: string + * example: Invalid base64 * 401: * description: Unauthorized - User is not authenticated * content: @@ -101,4 +122,197 @@ route.get('/', (req: Request, res: Response, next: NextFunction) => { }); }); +/** + * @swagger + * /api/protected/user/logo: + * post: + * summary: Update or create a user logo for current user + * tags: [User logo] + * security: + * - bearerAuth: [] # Indicates that authentication is required + * requestBody: + * required: true + * description: User logo base64 image + * content: + * application/json: + * schema: + * type: object + * properties: + * logo: + * type: string + * description: Base64 string of user logo + * example: "base64-image" + * responses: + * 200: + * description: User logo updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * type: + * type: string + * example: USER_LOGO_UPDATED + * statusCode: + * type: integer + * example: 200 + * message: + * type: string + * example: User logo updated successfully + * isSuccess: + * type: boolean + * example: true + * 401: + * description: Unauthorized - User is not authenticated + * content: + * application/json: + * schema: + * type: object + * properties: + * type: + * type: string + * example: ERROR + * statusCode: + * type: integer + * example: 401 + * message: + * type: string + * example: "Unauthorized" + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * type: + * type: string + * example: SERVER_ERROR + * statusCode: + * type: integer + * example: 500 + * message: + * type: string + * example: Internal server error + */ + +route.post('/logo', async (req: Request, res: Response, next: NextFunction) => { + try { + const user = req.user; + + if (!isAuthenticated(user)) { + return next(new UnauthorizedError(ErrorMessages.unauthorized)); + } + + const parsedBody = UpdateUserLogoSchema.safeParse(req.body); + + if (parsedBody.error) { + throw new UnprocessableEntityError(parsedBody.error.errors[0].message); + } + + await UserCrud.update({ + logo: parsedBody.data.logo, + userId: user.id, + }); + + return res.status(201).json({ + type: 'USER_LOGO_UPDATED', + statusCode: 201, + message: `User logo updated successfully.`, + isSuccess: true, + details: {}, + }); + } catch (error: unknown) { + return next(error); + } +}); + +/** + * @swagger + * /api/protected/user/logo: + * delete: + * summary: Delete a user logo for current user + * tags: [User] + * security: + * - bearerAuth: [] # Indicates that authentication is required + * responses: + * 200: + * description: User logo successfully deleted + * content: + * application/json: + * schema: + * type: object + * properties: + * type: + * type: string + * example: USER_LOGO_DELETED + * statusCode: + * type: integer + * example: 200 + * message: + * type: string + * example: User logo deleted successfully + * isSuccess: + * type: boolean + * example: true + * 401: + * description: Unauthorized - User is not authenticated + * content: + * application/json: + * schema: + * type: object + * properties: + * type: + * type: string + * example: ERROR + * statusCode: + * type: integer + * example: 401 + * message: + * type: string + * example: "Unauthorized" + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * type: + * type: string + * example: SERVER_ERROR + * statusCode: + * type: integer + * example: 500 + * message: + * type: string + * example: Internal server error + */ + +route.delete( + '/logo', + async (req: Request, res: Response, next: NextFunction) => { + const user = req.user; + + if (!isAuthenticated(user)) { + return next(new UnauthorizedError(ErrorMessages.unauthorized)); + } + + try { + await UserCrud.delete({ + userId: user.id, + }); + + return res.status(200).json({ + type: 'USER_LOGO_DELETED', + statusCode: 200, + message: 'User logo deleted successfully', + isSuccess: true, + }); + } catch (error: unknown) { + return next(error); + } + }, +); + export default route; diff --git a/src/api/user/user.crud.ts b/src/api/user/user.crud.ts new file mode 100644 index 0000000..f0091aa --- /dev/null +++ b/src/api/user/user.crud.ts @@ -0,0 +1,34 @@ +import { + deleteUserLogoDBPayload, + updateUserLogoDBPayload, +} from './validation.schema'; + +import { User } from '~/database/models/User'; + +export class UserCrud { + static update(payload: updateUserLogoDBPayload) { + return User.update( + { + logo: payload.logo, + }, + { + where: { + id: payload.userId, + }, + }, + ); + } + + static delete(payload: deleteUserLogoDBPayload) { + return User.update( + { + logo: null, + }, + { + where: { + id: payload.userId, + }, + }, + ); + } +} diff --git a/src/api/user/validation.schema.ts b/src/api/user/validation.schema.ts new file mode 100644 index 0000000..6c08048 --- /dev/null +++ b/src/api/user/validation.schema.ts @@ -0,0 +1,14 @@ +import z from 'zod'; + +export type updateUserLogoReqPayload = z.infer; + +export type deleteUserLogoDBPayload = { + userId: string; +}; + +export type updateUserLogoDBPayload = updateUserLogoReqPayload & + deleteUserLogoDBPayload; + +export const UpdateUserLogoSchema = z.object({ + logo: z.string().base64(), +}); diff --git a/src/database/migrations/20241218150018-add-logo-to-user.js b/src/database/migrations/20241218150018-add-logo-to-user.js new file mode 100644 index 0000000..c80225a --- /dev/null +++ b/src/database/migrations/20241218150018-add-logo-to-user.js @@ -0,0 +1,17 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn('user', 'logo', { + type: Sequelize.TEXT, + allowNull: true, + unique: false, + defaultValue: null, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.removeColumn('user', 'logo'); + }, +}; diff --git a/src/database/models/User.ts b/src/database/models/User.ts index 62e6ea5..881a54a 100644 --- a/src/database/models/User.ts +++ b/src/database/models/User.ts @@ -18,6 +18,10 @@ export const User = Sequelize.define( isEmail: true, }, }, + logo: { + type: DataTypes.TEXT, + allowNull: true, + }, }, {}, ); From 69f18ba8ea1b13f5accff5f529bca8930f8d0648 Mon Sep 17 00:00:00 2001 From: Vardan Mkhitaryan Date: Mon, 23 Dec 2024 11:53:43 +0400 Subject: [PATCH 2/5] Feat: user logo, updated swagger docs --- src/api/user/index.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/api/user/index.ts b/src/api/user/index.ts index f2911a1..c33e708 100644 --- a/src/api/user/index.ts +++ b/src/api/user/index.ts @@ -55,22 +55,6 @@ const route = express.Router(); * logo: * type: string * example: "Base64" - * 422: - * description: Unprocessable Entity - Invalid base64 - * content: - * application/json: - * schema: - * type: object - * properties: - * type: - * type: string - * example: UNPROCESSABLE_ENTITY_ERROR - * statusCode: - * type: integer - * example: 422 - * message: - * type: string - * example: Invalid base64 * 401: * description: Unauthorized - User is not authenticated * content: @@ -162,6 +146,22 @@ route.get('/', (req: Request, res: Response, next: NextFunction) => { * isSuccess: * type: boolean * example: true + * 422: + * description: Unprocessable Entity - Invalid base64 + * content: + * application/json: + * schema: + * type: object + * properties: + * type: + * type: string + * example: UNPROCESSABLE_ENTITY_ERROR + * statusCode: + * type: integer + * example: 422 + * message: + * type: string + * example: Invalid base64 * 401: * description: Unauthorized - User is not authenticated * content: From 613be47b65275484c0688abb3dd14776ce603092 Mon Sep 17 00:00:00 2001 From: Vardan Mkhitaryan Date: Mon, 23 Dec 2024 11:56:48 +0400 Subject: [PATCH 3/5] Feat: user logo, eslint fixes --- src/api/user/index.ts | 5 +++-- src/api/user/validation.schema.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/api/user/index.ts b/src/api/user/index.ts index c33e708..3f95a3d 100644 --- a/src/api/user/index.ts +++ b/src/api/user/index.ts @@ -1,10 +1,11 @@ import express, { NextFunction, Request, Response } from 'express'; +import { UserCrud } from './user.crud'; +import { UpdateUserLogoSchema } from './validation.schema'; + import { ErrorMessages } from '~/core/dictionary/error.messages'; import { UnauthorizedError, UnprocessableEntityError } from '~/core/errors'; import { isAuthenticated } from '~/shared/user'; -import { UpdateUserLogoSchema } from './validation.schema'; -import { UserCrud } from './user.crud'; const route = express.Router(); diff --git a/src/api/user/validation.schema.ts b/src/api/user/validation.schema.ts index 6c08048..af3c7fb 100644 --- a/src/api/user/validation.schema.ts +++ b/src/api/user/validation.schema.ts @@ -2,9 +2,9 @@ import z from 'zod'; export type updateUserLogoReqPayload = z.infer; -export type deleteUserLogoDBPayload = { +export interface deleteUserLogoDBPayload { userId: string; -}; +} export type updateUserLogoDBPayload = updateUserLogoReqPayload & deleteUserLogoDBPayload; From 1c36679bd224684eba14084141ff59ccde839a64 Mon Sep 17 00:00:00 2001 From: Vardan Mkhitaryan Date: Mon, 23 Dec 2024 12:00:41 +0400 Subject: [PATCH 4/5] Feat: user logo, swagger docs fixes --- src/api/user/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/user/index.ts b/src/api/user/index.ts index 3f95a3d..e146874 100644 --- a/src/api/user/index.ts +++ b/src/api/user/index.ts @@ -179,7 +179,7 @@ route.get('/', (req: Request, res: Response, next: NextFunction) => { * message: * type: string * example: "Unauthorized" - * 500: + * 500: * description: Internal server error * content: * application/json: @@ -272,7 +272,7 @@ route.post('/logo', async (req: Request, res: Response, next: NextFunction) => { * message: * type: string * example: "Unauthorized" - * 500: + * 500: * description: Internal server error * content: * application/json: From bf1bd0872bff91cdee72500bd14e906503544fbd Mon Sep 17 00:00:00 2001 From: Vardan Mkhitaryan Date: Tue, 24 Dec 2024 15:45:35 +0400 Subject: [PATCH 5/5] User logo refactor --- src/api/user/index.ts | 23 +++++++++++---------- src/api/user/user.crud.ts | 34 ------------------------------- src/api/user/validation.schema.ts | 14 ------------- src/shared/user/User.crud.ts | 23 ++++++++++++++++++++- src/shared/user/schema.ts | 9 ++++++++ 5 files changed, 43 insertions(+), 60 deletions(-) delete mode 100644 src/api/user/user.crud.ts delete mode 100644 src/api/user/validation.schema.ts diff --git a/src/api/user/index.ts b/src/api/user/index.ts index e146874..3f64172 100644 --- a/src/api/user/index.ts +++ b/src/api/user/index.ts @@ -1,11 +1,10 @@ import express, { NextFunction, Request, Response } from 'express'; -import { UserCrud } from './user.crud'; -import { UpdateUserLogoSchema } from './validation.schema'; - import { ErrorMessages } from '~/core/dictionary/error.messages'; import { UnauthorizedError, UnprocessableEntityError } from '~/core/errors'; import { isAuthenticated } from '~/shared/user'; +import { UpdateUserLogoSchema } from '~/shared/user/schema'; +import { UserCrudService } from '~/shared/user/User.crud'; const route = express.Router(); @@ -211,17 +210,21 @@ route.post('/logo', async (req: Request, res: Response, next: NextFunction) => { throw new UnprocessableEntityError(parsedBody.error.errors[0].message); } - await UserCrud.update({ - logo: parsedBody.data.logo, - userId: user.id, - }); + await UserCrudService.update( + { + logo: parsedBody.data.logo, + }, + user.id, + ); return res.status(201).json({ type: 'USER_LOGO_UPDATED', statusCode: 201, message: `User logo updated successfully.`, isSuccess: true, - details: {}, + details: { + user, + }, }); } catch (error: unknown) { return next(error); @@ -300,9 +303,7 @@ route.delete( } try { - await UserCrud.delete({ - userId: user.id, - }); + await UserCrudService.deleteLogo(user.id); return res.status(200).json({ type: 'USER_LOGO_DELETED', diff --git a/src/api/user/user.crud.ts b/src/api/user/user.crud.ts deleted file mode 100644 index f0091aa..0000000 --- a/src/api/user/user.crud.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - deleteUserLogoDBPayload, - updateUserLogoDBPayload, -} from './validation.schema'; - -import { User } from '~/database/models/User'; - -export class UserCrud { - static update(payload: updateUserLogoDBPayload) { - return User.update( - { - logo: payload.logo, - }, - { - where: { - id: payload.userId, - }, - }, - ); - } - - static delete(payload: deleteUserLogoDBPayload) { - return User.update( - { - logo: null, - }, - { - where: { - id: payload.userId, - }, - }, - ); - } -} diff --git a/src/api/user/validation.schema.ts b/src/api/user/validation.schema.ts deleted file mode 100644 index af3c7fb..0000000 --- a/src/api/user/validation.schema.ts +++ /dev/null @@ -1,14 +0,0 @@ -import z from 'zod'; - -export type updateUserLogoReqPayload = z.infer; - -export interface deleteUserLogoDBPayload { - userId: string; -} - -export type updateUserLogoDBPayload = updateUserLogoReqPayload & - deleteUserLogoDBPayload; - -export const UpdateUserLogoSchema = z.object({ - logo: z.string().base64(), -}); diff --git a/src/shared/user/User.crud.ts b/src/shared/user/User.crud.ts index 0cd5381..491ccff 100644 --- a/src/shared/user/User.crud.ts +++ b/src/shared/user/User.crud.ts @@ -1,6 +1,6 @@ import { Model } from 'sequelize'; -import { CreateUserSchemaType } from './schema'; +import { CreateUserSchemaType, UpdateUserDBPayload } from './schema'; import { UserCredential } from '../../database/models/UserCredential'; import { User } from '~/database/models/User'; @@ -39,4 +39,25 @@ export class UserCrudService { ], }); } + + static update(updatePayload: UpdateUserDBPayload, userId: string) { + return User.update(updatePayload, { + where: { + id: userId, + }, + }); + } + + static deleteLogo(userId: string) { + return User.update( + { + logo: null, + }, + { + where: { + id: userId, + }, + }, + ); + } } diff --git a/src/shared/user/schema.ts b/src/shared/user/schema.ts index 08ec525..ebdb4a2 100644 --- a/src/shared/user/schema.ts +++ b/src/shared/user/schema.ts @@ -7,3 +7,12 @@ export const CreateUserSchema = zod.object({ .max(255, 'Email length limit is 255 symbols') .email('Email should be a valid email address'), }); + +export type UpdateUserDBPayload = Partial<{ + logo: string; + email: string; +}>; + +export const UpdateUserLogoSchema = zod.object({ + logo: zod.string().base64(), +});