diff --git a/backend/src/bundles/auth/auth.controller.ts b/backend/src/bundles/auth/auth.controller.ts index 7a64ed90f..eaf4bb7a3 100644 --- a/backend/src/bundles/auth/auth.controller.ts +++ b/backend/src/bundles/auth/auth.controller.ts @@ -1,4 +1,8 @@ -import { type UserSignUpRequestDto } from '~/bundles/users/users.js'; +import { + type UserSignInRequestDto, + type UserSignUpRequestDto, + userSignInValidationSchema, +} from '~/bundles/users/users.js'; import { userSignUpValidationSchema } from '~/bundles/users/users.js'; import { type ApiHandlerOptions, @@ -20,6 +24,20 @@ class AuthController extends BaseController { this.authService = authService; + this.addRoute({ + path: AuthApiPath.SIGN_IN, + method: 'POST', + validation: { + body: userSignInValidationSchema, + }, + handler: (options) => + this.signIn( + options as ApiHandlerOptions<{ + body: UserSignInRequestDto; + }>, + ), + }); + this.addRoute({ path: AuthApiPath.SIGN_UP, method: 'POST', @@ -35,6 +53,55 @@ class AuthController extends BaseController { }); } + /** + * @swagger + * /auth/sign-in: + * post: + * description: Sign in user into the application + * requestBody: + * description: User auth data + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * format: email + * password: + * type: string + * responses: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: object + * $ref: '#/components/schemas/User' + * 400: + * description: Failed operation + * content: + * application/json: + * schema: + * type: object + * $ref: '#/components/schemas/Error' + */ + + private async signIn( + options: ApiHandlerOptions<{ + body: UserSignInRequestDto; + }>, + ): Promise { + return { + payload: await this.authService.signIn(options.body), + status: HttpCode.OK, + }; + } + /** * @swagger * /auth/sign-up: @@ -65,6 +132,7 @@ class AuthController extends BaseController { * type: object * $ref: '#/components/schemas/User' */ + private async signUp( options: ApiHandlerOptions<{ body: UserSignUpRequestDto; diff --git a/backend/src/bundles/auth/auth.service.ts b/backend/src/bundles/auth/auth.service.ts index 5ae168e63..fabf0d934 100644 --- a/backend/src/bundles/auth/auth.service.ts +++ b/backend/src/bundles/auth/auth.service.ts @@ -3,6 +3,14 @@ import { type UserSignUpResponseDto, } from '~/bundles/users/types/types.js'; import { type UserService } from '~/bundles/users/user.service.js'; +import { + type UserSignInRequestDto, + type UserSignInResponseDto, +} from '~/bundles/users/users.js'; +import { HttpCode, HttpError } from '~/common/http/http.js'; +import { cryptService } from '~/common/services/services.js'; + +import { UserValidationMessage } from './enums/enums.js'; class AuthService { private userService: UserService; @@ -11,6 +19,36 @@ class AuthService { this.userService = userService; } + public async signIn( + userRequestDto: UserSignInRequestDto, + ): Promise { + const { email, password } = userRequestDto; + const user = await this.userService.findByEmail(email); + + if (!user) { + throw new HttpError({ + message: UserValidationMessage.WRONG_CREDENTIALS, + status: HttpCode.BAD_REQUEST, + }); + } + + const { passwordHash } = user.toNewObject(); + + const isPwdCorrect = cryptService.compareSyncPassword( + password, + passwordHash, + ); + + if (!isPwdCorrect) { + throw new HttpError({ + message: UserValidationMessage.WRONG_CREDENTIALS, + status: HttpCode.BAD_REQUEST, + }); + } + + return user.toObject(); + } + public signUp( userRequestDto: UserSignUpRequestDto, ): Promise { diff --git a/backend/src/bundles/auth/enums/enums.ts b/backend/src/bundles/auth/enums/enums.ts index 7cbd1669d..e208cd7f4 100644 --- a/backend/src/bundles/auth/enums/enums.ts +++ b/backend/src/bundles/auth/enums/enums.ts @@ -1 +1 @@ -export { AuthApiPath } from 'shared'; +export { AuthApiPath, UserValidationMessage } from 'shared'; diff --git a/backend/src/bundles/users/types/types.ts b/backend/src/bundles/users/types/types.ts index f44313789..6a3620c41 100644 --- a/backend/src/bundles/users/types/types.ts +++ b/backend/src/bundles/users/types/types.ts @@ -1,5 +1,7 @@ export { type UserGetAllResponseDto, + type UserSignInRequestDto, + type UserSignInResponseDto, type UserSignUpRequestDto, type UserSignUpResponseDto, } from 'shared'; diff --git a/backend/src/bundles/users/user.repository.ts b/backend/src/bundles/users/user.repository.ts index be08919a1..b4d6110d8 100644 --- a/backend/src/bundles/users/user.repository.ts +++ b/backend/src/bundles/users/user.repository.ts @@ -13,6 +13,12 @@ class UserRepository implements Repository { return Promise.resolve(null); } + public async findByEmail(email: string): Promise { + const user = await this.userModel.query().findOne({ email }).execute(); + + return user ? UserEntity.initialize(user) : null; + } + public async findAll(): Promise { const users = await this.userModel.query().execute(); diff --git a/backend/src/bundles/users/user.service.ts b/backend/src/bundles/users/user.service.ts index 1616231f0..778c8f95f 100644 --- a/backend/src/bundles/users/user.service.ts +++ b/backend/src/bundles/users/user.service.ts @@ -20,6 +20,10 @@ class UserService implements Service { return Promise.resolve(null); } + public async findByEmail(email: string): Promise { + return await this.userRepository.findByEmail(email); + } + public async findAll(): Promise { const items = await this.userRepository.findAll(); diff --git a/backend/src/bundles/users/users.ts b/backend/src/bundles/users/users.ts index 1ad8ad701..73f640e85 100644 --- a/backend/src/bundles/users/users.ts +++ b/backend/src/bundles/users/users.ts @@ -11,8 +11,13 @@ const userController = new UserController(logger, userService); export { userController, userService }; export { + type UserSignInRequestDto, + type UserSignInResponseDto, type UserSignUpRequestDto, type UserSignUpResponseDto, } from './types/types.js'; export { UserModel } from './user.model.js'; -export { userSignUpValidationSchema } from './validation-schemas/validation-schemas.js'; +export { + userSignInValidationSchema, + userSignUpValidationSchema, +} from './validation-schemas/validation-schemas.js'; diff --git a/backend/src/bundles/users/validation-schemas/validation-schemas.ts b/backend/src/bundles/users/validation-schemas/validation-schemas.ts index 7bc9a09c5..5952fe0cf 100644 --- a/backend/src/bundles/users/validation-schemas/validation-schemas.ts +++ b/backend/src/bundles/users/validation-schemas/validation-schemas.ts @@ -1 +1 @@ -export { userSignUpValidationSchema } from 'shared'; +export { userSignInValidationSchema, userSignUpValidationSchema } from 'shared'; diff --git a/backend/src/common/server-application/base-server-app-api.ts b/backend/src/common/server-application/base-server-app-api.ts index 6630eaebc..e86461e11 100644 --- a/backend/src/common/server-application/base-server-app-api.ts +++ b/backend/src/common/server-application/base-server-app-api.ts @@ -38,11 +38,27 @@ class BaseServerAppApi implements ServerAppApi { definition: { openapi: '3.0.0', info: { - title: 'Hello World', + title: 'OutreachVids API documentation', version: `${this.version}.0.0`, }, + components: { + schemas: { + Error: { + type: 'object', + properties: { + errorType: { + type: 'string', + enum: ['COMMON', 'VALIDATION'], + }, + message: { + type: 'string', + }, + }, + }, + }, + }, }, - apis: [`src/packages/**/*.controller.${controllerExtension}`], + apis: [`src/bundles/**/*.controller.${controllerExtension}`], }); } } diff --git a/shared/src/bundles/auth/enums/auth-api-path.enum.ts b/shared/src/bundles/auth/enums/auth-api-path.enum.ts index 5833c6684..79d9340e1 100644 --- a/shared/src/bundles/auth/enums/auth-api-path.enum.ts +++ b/shared/src/bundles/auth/enums/auth-api-path.enum.ts @@ -1,5 +1,6 @@ const AuthApiPath = { ROOT: '/', + SIGN_IN: '/sign-in', SIGN_UP: '/sign-up', } as const; diff --git a/shared/src/bundles/users/enums/user-validation-message.enum.ts b/shared/src/bundles/users/enums/user-validation-message.enum.ts index b28063d96..1a168422a 100644 --- a/shared/src/bundles/users/enums/user-validation-message.enum.ts +++ b/shared/src/bundles/users/enums/user-validation-message.enum.ts @@ -1,6 +1,11 @@ const UserValidationMessage = { EMAIL_REQUIRE: 'Email is required', EMAIL_WRONG: 'Email is wrong', + EMAIL_INVALID: 'Please enter a valid email', + FIELD_REQUIRE: 'Please fill out this field', + PASSWORD_LENGTH: 'Password must have from 6 to 12 characters', + INVALID_DATA: 'Incorrect email or password. Please try again.', + WRONG_CREDENTIALS: 'Email or password are incorrect', } as const; export { UserValidationMessage }; diff --git a/shared/src/bundles/users/enums/user-validation-rule.enum.ts b/shared/src/bundles/users/enums/user-validation-rule.enum.ts index 36ca9c2e2..eb7d606aa 100644 --- a/shared/src/bundles/users/enums/user-validation-rule.enum.ts +++ b/shared/src/bundles/users/enums/user-validation-rule.enum.ts @@ -1,5 +1,8 @@ const UserValidationRule = { - EMAIL_MINIMUM_LENGTH: 1, + EMAIL_MINIMUM_LENGTH: 6, + EMAIL_MAXIMUM_LENGTH: 320, + PASSWORD_MINIMUM_LENGTH: 6, + PASSWORD_MAXIMUM_LENGTH: 12, } as const; export { UserValidationRule }; diff --git a/shared/src/bundles/users/types/types.ts b/shared/src/bundles/users/types/types.ts index 7564f70b2..36cfa4fdb 100644 --- a/shared/src/bundles/users/types/types.ts +++ b/shared/src/bundles/users/types/types.ts @@ -1,4 +1,6 @@ export { type UserGetAllItemResponseDto } from './user-get-all-item-response-dto.type.js'; export { type UserGetAllResponseDto } from './user-get-all-response-dto.type.js'; +export { type UserSignInRequestDto } from './user-sign-in-request-dto.type.js'; +export { type UserSignInResponseDto } from './user-sign-in-response-dto.type.js'; export { type UserSignUpRequestDto } from './user-sign-up-request-dto.type.js'; export { type UserSignUpResponseDto } from './user-sign-up-response-dto.type.js'; diff --git a/shared/src/bundles/users/types/user-sign-in-request-dto.type.ts b/shared/src/bundles/users/types/user-sign-in-request-dto.type.ts new file mode 100644 index 000000000..6d445fe1e --- /dev/null +++ b/shared/src/bundles/users/types/user-sign-in-request-dto.type.ts @@ -0,0 +1,5 @@ +import { type UserSignUpRequestDto } from './types.js'; + +type UserSignInRequestDto = Pick; + +export { type UserSignInRequestDto }; diff --git a/shared/src/bundles/users/types/user-sign-in-response-dto.type.ts b/shared/src/bundles/users/types/user-sign-in-response-dto.type.ts new file mode 100644 index 000000000..61be88ee0 --- /dev/null +++ b/shared/src/bundles/users/types/user-sign-in-response-dto.type.ts @@ -0,0 +1,5 @@ +import { type UserSignUpResponseDto } from './user-sign-up-response-dto.type.js'; + +type UserSignInResponseDto = UserSignUpResponseDto; + +export { type UserSignInResponseDto }; diff --git a/shared/src/bundles/users/users.ts b/shared/src/bundles/users/users.ts index e65858985..5a2147adc 100644 --- a/shared/src/bundles/users/users.ts +++ b/shared/src/bundles/users/users.ts @@ -2,7 +2,12 @@ export { UsersApiPath, UserValidationMessage } from './enums/enums.js'; export { type UserGetAllItemResponseDto, type UserGetAllResponseDto, + type UserSignInRequestDto, + type UserSignInResponseDto, type UserSignUpRequestDto, type UserSignUpResponseDto, } from './types/types.js'; -export { userSignUp as userSignUpValidationSchema } from './validation-schemas/validation-schemas.js'; +export { + userSignIn as userSignInValidationSchema, + userSignUp as userSignUpValidationSchema, +} from './validation-schemas/validation-schemas.js'; diff --git a/shared/src/bundles/users/validation-schemas/user-sig-in.validation-schema.ts b/shared/src/bundles/users/validation-schemas/user-sig-in.validation-schema.ts new file mode 100644 index 000000000..100cb3dad --- /dev/null +++ b/shared/src/bundles/users/validation-schemas/user-sig-in.validation-schema.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +import { UserValidationMessage, UserValidationRule } from '../enums/enums.js'; + +type UserSignInRequestValidationDto = { + email: z.ZodString; + password: z.ZodString; +}; + +const userSignIn = z + .object({ + email: z + .string({ required_error: UserValidationMessage.FIELD_REQUIRE }) + .trim() + .min(UserValidationRule.EMAIL_MINIMUM_LENGTH, { + message: UserValidationMessage.EMAIL_INVALID, + }) + .max(UserValidationRule.EMAIL_MAXIMUM_LENGTH, { + message: UserValidationMessage.EMAIL_INVALID, + }) + .email({ + message: UserValidationMessage.EMAIL_INVALID, + }), + password: z + .string({ required_error: UserValidationMessage.FIELD_REQUIRE }) + .trim() + .min(UserValidationRule.PASSWORD_MINIMUM_LENGTH, { + message: UserValidationMessage.PASSWORD_LENGTH, + }) + .max(UserValidationRule.PASSWORD_MAXIMUM_LENGTH, { + message: UserValidationMessage.PASSWORD_LENGTH, + }), + }) + .required(); + +export { userSignIn }; diff --git a/shared/src/bundles/users/validation-schemas/validation-schemas.ts b/shared/src/bundles/users/validation-schemas/validation-schemas.ts index cb1c2ad60..58cd817cf 100644 --- a/shared/src/bundles/users/validation-schemas/validation-schemas.ts +++ b/shared/src/bundles/users/validation-schemas/validation-schemas.ts @@ -1 +1,2 @@ +export { userSignIn } from './user-sig-in.validation-schema.js'; export { userSignUp } from './user-sign-up.validation-schema.js'; diff --git a/shared/src/framework/http/enums/http-code.enum.ts b/shared/src/framework/http/enums/http-code.enum.ts index 59a238f46..6d2ad89ba 100644 --- a/shared/src/framework/http/enums/http-code.enum.ts +++ b/shared/src/framework/http/enums/http-code.enum.ts @@ -1,6 +1,7 @@ const HttpCode = { OK: 200, CREATED: 201, + BAD_REQUEST: 400, UNPROCESSED_ENTITY: 422, INTERNAL_SERVER_ERROR: 500, } as const; diff --git a/shared/src/index.ts b/shared/src/index.ts index df887038b..d4b77ee0e 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -2,10 +2,14 @@ export { AuthApiPath } from './bundles/auth/auth.js'; export { type UserGetAllItemResponseDto, type UserGetAllResponseDto, + type UserSignInRequestDto, + type UserSignInResponseDto, type UserSignUpRequestDto, type UserSignUpResponseDto, UsersApiPath, + userSignInValidationSchema, userSignUpValidationSchema, + UserValidationMessage, } from './bundles/users/users.js'; export { ApiPath,