diff --git a/src/api/user/index.ts b/src/api/user/index.ts index 8eed6d4..3f64172 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 '~/shared/user/schema'; +import { UserCrudService } from '~/shared/user/User.crud'; const route = express.Router(); @@ -50,6 +52,9 @@ const route = express.Router(); * name: * type: string * example: "John Doe" + * logo: + * type: string + * example: "Base64" * 401: * description: Unauthorized - User is not authenticated * content: @@ -101,4 +106,215 @@ 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 + * 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: + * 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 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: { + user, + }, + }); + } 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 UserCrudService.deleteLogo(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/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, + }, }, {}, ); 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(), +});