diff --git a/src/v6y-bff/src/resolvers/account/AccountMutations.ts b/src/v6y-bff/src/resolvers/account/AccountMutations.ts index e50e7e25..180e7062 100644 --- a/src/v6y-bff/src/resolvers/account/AccountMutations.ts +++ b/src/v6y-bff/src/resolvers/account/AccountMutations.ts @@ -1,6 +1,8 @@ import { AccountInputType, AccountProvider, + AccountType, + AccountUpdatePasswordType, AppLogger, PasswordUtils, SearchQueryType, @@ -12,32 +14,52 @@ const { hashPassword } = PasswordUtils; * Create or edit account * @param _ * @param params + * @param context */ -const createOrEditAccount = async (_: unknown, params: { accountInput: AccountInputType }) => { +const createOrEditAccount = async ( + _: unknown, + params: { input: AccountInputType }, + context: { user: AccountType }, +) => { try { - const { _id, username, password, email, role, applications } = params?.accountInput || {}; + const { _id, username, password, email, role, applications } = params?.input || {}; AppLogger.info(`[AccountMutations - createOrEditAccount] _id : ${_id}`); AppLogger.info(`[AccountMutations - createOrEditAccount] username : ${username}`); AppLogger.info(`[AccountMutations - createOrEditAccount] password : ${password}`); AppLogger.info(`[AccountMutations - createOrEditAccount] email : ${email}`); AppLogger.info(`[AccountMutations - createOrEditAccount] role : ${role}`); - AppLogger.info( - `[AccountMutations - createOrEditAccount] applications : ${applications?.join(',')}`, - ); + AppLogger.info(`[AccountMutations - createOrEditAccount] applications : ${applications}`); if (_id) { - const editedAccount = await AccountProvider.editAccount({ - _id, - username, - password: await hashPassword(password), - email, - role, - applications, - }); + let editedAccount = null; + if (!password) { + editedAccount = await AccountProvider.editAccount({ + account: { + _id, + username, + email, + role, + applications, + }, + currentUser: context.user, + }); + } else { + editedAccount = await AccountProvider.editAccount({ + account: { + _id, + username, + password: await hashPassword(password), + email, + role, + applications, + }, + currentUser: context.user, + }); + } if (!editedAccount || !editedAccount._id) { - return null; + throw new Error('Invalid account'); } AppLogger.info( @@ -49,6 +71,14 @@ const createOrEditAccount = async (_: unknown, params: { accountInput: AccountIn }; } + if (!password) { + throw new Error('Password is required'); + } + + const user = await AccountProvider.getAccountDetailsByParams({ email }); + if (user) { + throw new Error('User already exists with this email'); + } const createdAccount = await AccountProvider.createAccount({ username, password: await hashPassword(password), @@ -74,22 +104,78 @@ const createOrEditAccount = async (_: unknown, params: { accountInput: AccountIn } }; +/** + * Update password + * @param _ + * @param params + * @param context + **/ +const updateAccountPassword = async ( + _: unknown, + params: { input: AccountUpdatePasswordType }, + context: { user: AccountType }, +) => { + try { + const { _id, password } = params?.input || {}; + + AppLogger.info(`[AccountMutations - updatePassword] _id : ${_id}`); + + const accountDetails = await AccountProvider.getAccountDetailsByParams({ _id }); + + if (!accountDetails) { + throw new Error('Invalid account'); + } + + const updatedAccount = await AccountProvider.updateAccountPassword({ + _id, + password: await PasswordUtils.hashPassword(password), + currentUser: context.user, + }); + + if (!updatedAccount || !updatedAccount._id) { + throw new Error('Invalid account'); + } + + AppLogger.info( + `[AccountMutations - updatePassword] updatedAccount : ${updatedAccount._id}`, + ); + + return { + _id: updatedAccount._id, + }; + } catch (error) { + AppLogger.error(`[AccountMutations - updatePassword] error : ${error}`); + return null; + } +}; + /** * Delete account * @param _ * @param params */ -const deleteAccount = async (_: unknown, params: { input: SearchQueryType }) => { +const deleteAccount = async ( + _: unknown, + params: { input: SearchQueryType }, + context: { user: AccountType }, +) => { try { const whereClause = params?.input?.where; - if (!whereClause) { + + if (!whereClause?.id) { return null; } - - const accountId = whereClause._id; + const accountId = parseInt(whereClause.id, 10); AppLogger.info(`[AccountMutations - deleteAccount] accountId : ${accountId}`); - await AccountProvider.deleteAccount({ _id: accountId }); + const user = await AccountProvider.getAccountDetailsByParams({ + _id: accountId, + }); + if (!user) { + throw new Error('User does not exist'); + } + + await AccountProvider.deleteAccount({ userToDelete: user, currentUser: context.user }); AppLogger.info(`[AccountMutations - deleteAccount] deleted account : ${accountId}`); return { _id: accountId, @@ -102,6 +188,7 @@ const deleteAccount = async (_: unknown, params: { input: SearchQueryType }) => const AccountMutations = { createOrEditAccount, + updateAccountPassword, deleteAccount, }; diff --git a/src/v6y-bff/src/resolvers/account/AccountQueries.ts b/src/v6y-bff/src/resolvers/account/AccountQueries.ts index 9d8786c9..a6276ac4 100644 --- a/src/v6y-bff/src/resolvers/account/AccountQueries.ts +++ b/src/v6y-bff/src/resolvers/account/AccountQueries.ts @@ -28,11 +28,6 @@ const getAccountDetailsByParams = async (_: unknown, args: AccountType) => { const accountDetails = await AccountProvider.getAccountDetailsByParams({ _id, }); - - AppLogger.info( - `[AccountQueries - getAccountDetailsByParams] accountDetails : ${accountDetails?._id}`, - ); - return accountDetails; } catch (error) { AppLogger.info(`[AccountQueries - getAccountDetailsByParams] error : ${error}`); @@ -105,11 +100,14 @@ const loginAccount = async (_: unknown, params: { input: AccountLoginType }) => const token = generateAuthenticationToken(accountDetails); - AppLogger.info(`[AccountMutations - loginAccount] login success : ${accountDetails._id}`); + AppLogger.info( + `[AccountMutations - loginAccount] login success : ${accountDetails._id} - ${accountDetails.role}`, + ); return { _id: accountDetails._id, token, + role: accountDetails.role, }; } catch (error) { AppLogger.info(`[AccountMutations - loginAccount] error : ${error}`); diff --git a/src/v6y-bff/src/resolvers/application/ApplicationQueries.ts b/src/v6y-bff/src/resolvers/application/ApplicationQueries.ts index 09f52416..4d8a112b 100644 --- a/src/v6y-bff/src/resolvers/application/ApplicationQueries.ts +++ b/src/v6y-bff/src/resolvers/application/ApplicationQueries.ts @@ -179,6 +179,34 @@ const getApplicationListByPageAndParams = async (_: unknown, args: SearchQueryTy } }; +/** + * Get application list + * @param _ + * @param args + */ +const getApplicationList = async (_: unknown, args: SearchQueryType) => { + try { + const { where, sort } = args || {}; + + AppLogger.info(`[ApplicationQueries - getApplicationListByPageAndParams] where : ${where}`); + AppLogger.info(`[ApplicationQueries - getApplicationListByPageAndParams] sort : ${sort}`); + + const appList = await ApplicationProvider.getApplicationListByPageAndParams({ + where, + sort, + }); + + AppLogger.info( + `[ApplicationQueries - getApplicationListByPageAndParams] appList : ${appList?.length}`, + ); + + return appList; + } catch (error) { + AppLogger.info(`[ApplicationQueries - getApplicationListByPageAndParams] error : ${error}`); + return []; + } +}; + /** * Get application stats by params * @param _ @@ -251,6 +279,7 @@ const ApplicationQueries = { getApplicationDetailsKeywordsByParams, getApplicationTotalByParams, getApplicationListByPageAndParams, + getApplicationList, getApplicationStatsByParams, }; diff --git a/src/v6y-bff/src/types/VitalityTypes.ts b/src/v6y-bff/src/types/VitalityTypes.ts index 831aa2de..95656661 100644 --- a/src/v6y-bff/src/types/VitalityTypes.ts +++ b/src/v6y-bff/src/types/VitalityTypes.ts @@ -9,6 +9,8 @@ import AccountLoginOutput from './account/AccountLoginOutput.ts'; import AccountMutationsType from './account/AccountMutationsType.ts'; import AccountQueriesType from './account/AccountQueriesType.ts'; import AccountType from './account/AccountType.ts'; +import AccountUpdatePasswordInput from './account/AccountUpdatePasswordInput.ts'; +import AccountUpdatePasswordOutput from './account/AccountUpdatePasswordOutput.ts'; import ApplicationCreateOrEditInput from './application/ApplicationCreateOrEditInput.ts'; import ApplicationDeleteInput from './application/ApplicationDeleteInput.ts'; import ApplicationDeleteOutput from './application/ApplicationDeleteOutput.ts'; @@ -134,6 +136,8 @@ const VitalityTypes = gql(` ${AccountCreateOrEditInput} ${AccountCreateOrEditOutput} + ${AccountUpdatePasswordInput} + ${AccountUpdatePasswordOutput} ${AccountDeleteInput} ${AccountDeleteOutput} ${AccountMutationsType} diff --git a/src/v6y-bff/src/types/account/AccountCreateOrEditInput.ts b/src/v6y-bff/src/types/account/AccountCreateOrEditInput.ts index c2d5ee42..d5dd4ede 100644 --- a/src/v6y-bff/src/types/account/AccountCreateOrEditInput.ts +++ b/src/v6y-bff/src/types/account/AccountCreateOrEditInput.ts @@ -10,7 +10,7 @@ const AccountCreateOrEditInput = ` username: String! """ Account Password """ - password: String! + password: String """ Account Role """ role: String! diff --git a/src/v6y-bff/src/types/account/AccountDeleteInput.ts b/src/v6y-bff/src/types/account/AccountDeleteInput.ts index 09cc39a4..65baae25 100644 --- a/src/v6y-bff/src/types/account/AccountDeleteInput.ts +++ b/src/v6y-bff/src/types/account/AccountDeleteInput.ts @@ -1,7 +1,7 @@ const AccountDeleteInput = ` input AccountDeleteInputClause { """ Account to delete id """ - _id: Int! + id: String! } input AccountDeleteInput { diff --git a/src/v6y-bff/src/types/account/AccountLoginOutput.ts b/src/v6y-bff/src/types/account/AccountLoginOutput.ts index 3dcf02db..d9c0db0c 100644 --- a/src/v6y-bff/src/types/account/AccountLoginOutput.ts +++ b/src/v6y-bff/src/types/account/AccountLoginOutput.ts @@ -4,6 +4,8 @@ const AccountLoginOutput = ` _id: Int! """ Account token """ token: String! + """ Account role """ + role: String! } `; diff --git a/src/v6y-bff/src/types/account/AccountMutationsType.ts b/src/v6y-bff/src/types/account/AccountMutationsType.ts index f81341f0..c3d937c9 100644 --- a/src/v6y-bff/src/types/account/AccountMutationsType.ts +++ b/src/v6y-bff/src/types/account/AccountMutationsType.ts @@ -1,6 +1,7 @@ const AccountMutationsType = ` type Mutation { - createOrEditAccount(accountInput: AccountCreateOrEditInput!): AccountCreateOrEditOutput + createOrEditAccount(input: AccountCreateOrEditInput!): AccountCreateOrEditOutput + updateAccountPassword(input: AccountUpdatePasswordInput!): AccountUpdatePasswordOutput deleteAccount(input: AccountDeleteInput!): AccountDeleteOutput } `; diff --git a/src/v6y-bff/src/types/account/AccountUpdatePasswordInput.ts b/src/v6y-bff/src/types/account/AccountUpdatePasswordInput.ts new file mode 100644 index 00000000..3ae99816 --- /dev/null +++ b/src/v6y-bff/src/types/account/AccountUpdatePasswordInput.ts @@ -0,0 +1,11 @@ +const AccountUpdatePasswordInput = ` + input AccountUpdatePasswordInput { + """ Account Id """ + _id: Int! + + """ Account New Password """ + password : String! + } +`; + +export default AccountUpdatePasswordInput; diff --git a/src/v6y-bff/src/types/account/AccountUpdatePasswordOutput.ts b/src/v6y-bff/src/types/account/AccountUpdatePasswordOutput.ts new file mode 100644 index 00000000..8ce116bc --- /dev/null +++ b/src/v6y-bff/src/types/account/AccountUpdatePasswordOutput.ts @@ -0,0 +1,8 @@ +const AccountUpdatePasswordOutput = ` + type AccountUpdatePasswordOutput { + """ Account id """ + _id: Int! + } +`; + +export default AccountUpdatePasswordOutput; diff --git a/src/v6y-bff/src/types/application/ApplicationQueriesType.ts b/src/v6y-bff/src/types/application/ApplicationQueriesType.ts index bd42f410..bcd8504a 100644 --- a/src/v6y-bff/src/types/application/ApplicationQueriesType.ts +++ b/src/v6y-bff/src/types/application/ApplicationQueriesType.ts @@ -1,5 +1,6 @@ const ApplicationQueriesType = ` type Query { + getApplicationList(where: JSON, sort: [String]): [ApplicationType] getApplicationListByPageAndParams(start: Int, offset: Int, limit: Int, keywords: [String], searchText: String, where: JSON, sort: String): [ApplicationType] getApplicationStatsByParams(keywords: [String]): [KeywordStatsType] getApplicationTotalByParams(keywords: [String], searchText: String): Int diff --git a/src/v6y-commons/eslint.config.mjs b/src/v6y-commons/eslint.config.mjs index e2e3c435..a9bd7c9c 100644 --- a/src/v6y-commons/eslint.config.mjs +++ b/src/v6y-commons/eslint.config.mjs @@ -7,7 +7,7 @@ export default [ ...tsEslint.configs.recommended, { files: ['src/**/*.js', 'src/**/*.mjs', 'src/**/*.tsx', 'src/**/*.ts'], - ignores: ['**/*.test.js', '*.d.ts'], + ignores: ['**/*test.js', '**/*test.ts', '*.d.ts'], rules: { 'max-depth': ['error', 3], 'max-nested-callbacks': ['error', 3], diff --git a/src/v6y-commons/src/core/AuthenticationHelper.ts b/src/v6y-commons/src/core/AuthenticationHelper.ts index 3eaff213..f605f9a0 100644 --- a/src/v6y-commons/src/core/AuthenticationHelper.ts +++ b/src/v6y-commons/src/core/AuthenticationHelper.ts @@ -34,9 +34,13 @@ export const createJwtOptions = () => { * Creates a verification function for JWT strategy. * @returns {Function} A function that verifies JWT payload. */ -const createJwtStrategyVerify = () => { +export const createJwtStrategyVerify = () => { return async (jwtPayload: JwtPayload, done: VerifiedCallback) => { try { + AppLogger.info( + `[AuthenticationHelper - createJwtStrategyVerify] JwtPayload: ${JwtPayload?._id}`, + ); + // Ensure the token contains the `_id` field. if (!jwtPayload._id) { return done(Error('Token does not contain _id'), undefined); @@ -46,6 +50,10 @@ const createJwtStrategyVerify = () => { _id: jwtPayload._id, }); + AppLogger.info( + `[AuthenticationHelper - createJwtStrategyVerify] _id : ${accountDetails?._id}`, + ); + if (!accountDetails) { return done(Error('User not Found'), undefined); } @@ -53,6 +61,7 @@ const createJwtStrategyVerify = () => { AppLogger.info( `[AuthenticationHelper - createJwtStrategyVerify] User Found : ${accountDetails._id}`, ); + return done(null, accountDetails); } catch (error) { AppLogger.error(`[AuthenticationHelper- createJwtStrategyVerify] : ${error}`); @@ -104,6 +113,20 @@ export const validateCredentials = (request: T): Promise => { */ export const configureAuthMiddleware = (): T => passport.initialize() as T; +/** + * Check if user role is ADMIN. + * @param {object} account + * @returns {boolean} + */ +export const isAdmin = (account: AccountType) => account?.role === 'ADMIN'; + +/** + * Check if user role is SUPERADMIN. + * @param {object} account + * @returns {boolean} + */ +export const isSuperAdmin = (account: AccountType) => account?.role === 'SUPERADMIN'; + /** * Configures the authentication strategy. */ diff --git a/src/v6y-commons/src/core/__tests__/AuthenticationHelper-test.ts b/src/v6y-commons/src/core/__tests__/AuthenticationHelper-test.ts index 5d4063c3..e6854cfb 100644 --- a/src/v6y-commons/src/core/__tests__/AuthenticationHelper-test.ts +++ b/src/v6y-commons/src/core/__tests__/AuthenticationHelper-test.ts @@ -1,117 +1,197 @@ -// AuthenticationHelper.tests.ts -import { Mock, describe, expect, it, vi } from 'vitest'; +import passport from 'passport'; +import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import AccountProvider from '../../database/AccountProvider.ts'; -import * as AuthenticationHelper from '../AuthenticationHelper.ts'; - -vi.mock('../../database/AccountProvider.ts', async () => { - const actualModule = (await vi.importActual( - '../../database/AccountProvider.ts', - )) as typeof import('../../database/AccountProvider.ts'); - - return { - default: { - ...actualModule.default, - getAccountDetailsByParams: vi.fn(), - }, - }; -}); +import { AccountType } from '../../types/AccountType.ts'; +import AppLogger from '../AppLogger.ts'; +import { + configureAuthenticationStrategy, + createJwtStrategyVerify, + generateAuthenticationToken, + isAdmin, + isSuperAdmin, + validateCredentials, +} from '../AuthenticationHelper.ts'; + +vi.mock('jsonwebtoken'); +vi.mock('passport'); +vi.mock('../../database/AccountProvider.ts', () => ({ + default: { + getAccountDetailsByParams: vi.fn(), + }, +})); +vi.mock('../AppLogger.ts'); describe('AuthenticationHelper', () => { - it('should create jwt options', () => { - const result = AuthenticationHelper.createJwtOptions(); - expect(result).toEqual({ - jwtFromRequest: expect.any(Function), - secretOrKey: expect.any(String), + const mockAccount: AccountType = { + _id: 1, + username: 'testuser', + email: 'test@example.com', + password: 'password', + role: 'USER', + applications: [1], + }; + + beforeEach(() => { + vi.clearAllMocks(); + delete process.env.JWT_SECRET; + }); + + it('should verify JWT payload and return account details', async () => { + const jwtPayload = { _id: 1 }; + (AccountProvider.getAccountDetailsByParams as Mock).mockResolvedValue(mockAccount); + const verify = createJwtStrategyVerify(); + const done = vi.fn(); + await verify(jwtPayload, done); + expect(AccountProvider.getAccountDetailsByParams).toHaveBeenCalledWith({ _id: 1 }); + expect(done).toHaveBeenCalledWith(null, mockAccount); + }); + + it('should return error if token does not contain _id', async () => { + const jwtPayload = {}; + const verify = createJwtStrategyVerify(); + const done = vi.fn(); + await verify(jwtPayload, done); + expect(done).toHaveBeenCalledWith(Error('Token does not contain _id'), undefined); + }); + + it('should return error if user is not found', async () => { + const jwtPayload = { _id: 1 }; + (AccountProvider.getAccountDetailsByParams as Mock).mockResolvedValue(null); + const verify = createJwtStrategyVerify(); + const done = vi.fn(); + await verify(jwtPayload, done); + expect(AccountProvider.getAccountDetailsByParams).toHaveBeenCalledWith({ _id: 1 }); + expect(done).toHaveBeenCalledWith(Error('User not Found'), undefined); + }); + + it('should handle errors during verification', async () => { + const jwtPayload = { _id: 1 }; + (AccountProvider.getAccountDetailsByParams as Mock).mockRejectedValue( + new Error('DB error'), + ); + const verify = createJwtStrategyVerify(); + const done = vi.fn(); + await verify(jwtPayload, done); + expect(done).toHaveBeenCalledWith(Error('DB error'), undefined); + expect(AppLogger.error).toHaveBeenCalledWith( + '[AuthenticationHelper- createJwtStrategyVerify] : Error: DB error', + ); + }); + + it('should authenticate user and resolve with user details', async () => { + const request = {}; + (passport.authenticate as Mock).mockImplementation((strategy, options, callback) => { + callback(null, mockAccount); }); + const user = await validateCredentials(request); + expect(passport.authenticate).toHaveBeenCalledWith( + 'jwt', + { session: false }, + expect.any(Function), + ); + expect(user).toEqual(mockAccount); + }); + + it('should resolve with null if authentication fails', async () => { + const request = {}; + (passport.authenticate as Mock).mockImplementation((strategy, options, callback) => { + callback(new Error('Authentication failed'), null); + }); + const user = await validateCredentials(request); + expect(passport.authenticate).toHaveBeenCalledWith( + 'jwt', + { session: false }, + expect.any(Function), + ); + expect(user).toEqual(null); + expect(AppLogger.error).toHaveBeenCalledWith( + '[AuthenticationHelper - validateCredentials] Not authenticated : Error: Authentication failed', + ); + }); + + it('should return true if user role is ADMIN', () => { + expect(isAdmin({ ...mockAccount, role: 'ADMIN' })).toBe(true); + }); + + it('should return false if user role is not ADMIN', () => { + expect(isAdmin(mockAccount)).toBe(false); }); - it('should generate token', () => { - const account = { _id: 12 }; - const result = AuthenticationHelper.generateAuthenticationToken(account); - - expect(result).toEqual(expect.any(String)); - }); - - it('should return null if token is invalid', async () => { - const account = {}; - const token = AuthenticationHelper.generateAuthenticationToken(account); - - const request = { - cache: 'default' as RequestCache, - credentials: 'same-origin', - destination: '', - integrity: '', - keepalive: false, - method: 'GET', - mode: 'cors', - redirect: 'follow', - referrer: '', - referrerPolicy: '', - headers: { - authorization: `Bearer ${token}`, - }, - url: '', - clone: () => request, - body: null, - bodyUsed: false, - context: '', - signal: new AbortController().signal, - arrayBuffer: async () => new ArrayBuffer(0), - blob: async () => new Blob(), - formData: async () => new FormData(), - json: async () => ({}), - text: async () => '', - } as unknown as Request; - - const result = await AuthenticationHelper.validateCredentials(request); - expect(result).toBe(null); - }); - - it('should authenticate with passport', async () => { - (AccountProvider.getAccountDetailsByParams as Mock).mockReturnValue({ - _id: 15, - email: 'superadmin@superadmin.superadmin', - role: 'ADMIN', - applications: [1, 2, 3], + it('should return true if user role is SUPERADMIN', () => { + expect(isSuperAdmin({ ...mockAccount, role: 'SUPERADMIN' })).toBe(true); + }); + + it('should return false if user role is not SUPERADMIN', () => { + expect(isSuperAdmin(mockAccount)).toBe(false); + }); + + it('should configure JWT strategy', () => { + process.env.JWT_SECRET = 'test_secret'; + configureAuthenticationStrategy(); + expect(passport.use).toHaveBeenCalledWith(expect.any(Object)); + }); + + it('should configure JWT strategy', () => { + process.env.JWT_SECRET = 'test_secret'; + configureAuthenticationStrategy(); + expect(passport.use).toHaveBeenCalledWith(expect.any(Object)); + }); + + it('should throw an error when generating a JWT token if JWT_SECRET is undefined', () => { + delete process.env.JWT_SECRET; + const account = { _id: 1, email: 'test@example.com' }; + expect(() => generateAuthenticationToken(account)).toThrow( + '[AuthenticationHelper - generateAuthenticationToken] JWT_SECRET is not defined in the environment variables', + ); + }); + + it('should correctly initialize passport middleware', () => { + const middleware = passport.initialize(); + expect(passport.initialize).toHaveBeenCalled(); + expect(middleware).toBe(passport.initialize()); + }); + + it('should call AppLogger when an error occurs during JWT strategy verification', async () => { + const jwtPayload = { _id: 1 }; + (AccountProvider.getAccountDetailsByParams as Mock).mockRejectedValue( + new Error('Unexpected error'), + ); + + const verify = createJwtStrategyVerify(); + const done = vi.fn(); + await verify(jwtPayload, done); + + expect(AppLogger.error).toHaveBeenCalledWith( + '[AuthenticationHelper- createJwtStrategyVerify] : Error: Unexpected error', + ); + expect(done).toHaveBeenCalledWith(Error('Unexpected error'), undefined); + }); + + it('should log user information during successful JWT strategy verification', async () => { + const jwtPayload = { _id: 1 }; + (AccountProvider.getAccountDetailsByParams as Mock).mockResolvedValue(mockAccount); + + const verify = createJwtStrategyVerify(); + const done = vi.fn(); + await verify(jwtPayload, done); + + expect(AppLogger.info).toHaveBeenCalledWith( + '[AuthenticationHelper - createJwtStrategyVerify] _id : 1', + ); + }); + + it('should log an error if no strategy is configured during authentication', () => { + process.env.JWT_SECRET = 'test_secret'; + + (passport.use as Mock).mockImplementation(() => { + throw new Error('No strategy configured'); }); - const account = { - _id: 15, - email: 'superadmin@superadmin.superadmin', - role: 'ADMIN', - applications: [1, 2, 3], - }; - const token = AuthenticationHelper.generateAuthenticationToken(account); - const request = { - cache: 'default' as RequestCache, - credentials: 'same-origin', - destination: '', - integrity: '', - keepalive: false, - method: 'GET', - mode: 'cors', - redirect: 'follow', - referrer: '', - referrerPolicy: '', - headers: { - authorization: `Bearer ${token}`, - }, - url: '', - clone: () => request, - body: null, - bodyUsed: false, - context: '', - signal: new AbortController().signal, - arrayBuffer: async () => new ArrayBuffer(0), - blob: async () => new Blob(), - formData: async () => new FormData(), - json: async () => ({}), - text: async () => '', - } as unknown as Request; - - const result = await AuthenticationHelper.validateCredentials(request); - - expect(result).toEqual(account); + configureAuthenticationStrategy(); + + expect(AppLogger.error).toHaveBeenCalledWith( + '[AuthenticationHelper - validateCredentials] Not authenticated : Error: No strategy configured', + ); }); }); diff --git a/src/v6y-commons/src/database/AccountProvider.ts b/src/v6y-commons/src/database/AccountProvider.ts index 885620d2..3935de6f 100644 --- a/src/v6y-commons/src/database/AccountProvider.ts +++ b/src/v6y-commons/src/database/AccountProvider.ts @@ -1,6 +1,7 @@ import { FindOptions, Op, Sequelize } from 'sequelize'; import AppLogger from '../core/AppLogger.ts'; +import { isAdmin, isSuperAdmin } from '../core/AuthenticationHelper.ts'; import { AccountInputType, AccountType } from '../types/AccountType.ts'; import { SearchQueryType } from '../types/SearchQueryType.ts'; import { AccountModelType } from './models/AccountModel.ts'; @@ -24,7 +25,7 @@ const buildSearchQuery = async ({ queryOptions.limit = limit; if (sort) { - queryOptions.order = [[sort, 'ASC']]; + // queryOptions.order = [[sort, 'ASC']]; } if (searchText) { @@ -82,8 +83,26 @@ const createAccount = async (account: AccountInputType) => { * Edit an Account * @param account */ -const editAccount = async (account: AccountInputType) => { +const editAccount = async ({ + account, + currentUser, +}: { + account: AccountInputType; + currentUser: AccountType; +}) => { try { + if (!(isAdmin(currentUser) || isSuperAdmin(currentUser))) { + throw new Error('You are not authorized to create an account'); + } + + if ( + !isSuperAdmin(currentUser) && + (account.role === 'ADMIN' || account.role === 'SUPERADMIN') + ) { + AppLogger.info(`[AccountProvider - createOrEditAccount] role : ${account.role}`); + throw new Error('You are not authorized to create an admin account'); + } + AppLogger.info(`[AccountProvider - editAccount] account id: ${account?._id}`); AppLogger.info(`[AccountProvider - editAccount] account username: ${account?.username}`); AppLogger.info(`[AccountProvider - editAccount] account role: ${account?.role}`); @@ -118,28 +137,101 @@ const editAccount = async (account: AccountInputType) => { } }; +/** + * Update Account Password + * @param account + */ + +const updateAccountPassword = async ({ + _id, + password, + currentUser, +}: { + _id: number; + password: string; + currentUser: AccountType; +}) => { + try { + if (currentUser._id !== _id && !isAdmin(currentUser) && !isSuperAdmin(currentUser)) { + throw new Error('You are not authorized to update this account'); + } + + if (!_id || !password) { + return null; + } + + AppLogger.info(`[AccountProvider - updateAccountPassword] _id: ${_id}`); + + const accountDetails = await AccountModelType.findOne({ + where: { + _id, + }, + }); + + if (!accountDetails) { + return null; + } + + await AccountModelType.update( + { + password: password, + }, + { + where: { + _id, + }, + }, + ); + + return { + _id, + }; + } catch (error) { + AppLogger.info(`[AccountProvider - updateAccountPassword] error: ${error}`); + return null; + } +}; + /** * Delete an Account * @param _id */ -const deleteAccount = async ({ _id }: AccountType) => { +const deleteAccount = async ({ + userToDelete, + currentUser, +}: { + userToDelete: AccountType; + currentUser: AccountType; +}) => { try { - AppLogger.info(`[AccountProvider - deleteAccount] _id: ${_id}`); + AppLogger.info(`[AccountProvider - deleteAccount] _id: ${userToDelete._id}`); - if (!_id) { + if (!(isSuperAdmin(currentUser) || isAdmin(currentUser))) { + throw new Error('You are not authorized to delete an account'); + } + + if (currentUser._id === userToDelete._id) { + throw new Error('You cannot delete your own account'); + } + + if (userToDelete.role === 'ADMIN' && !isSuperAdmin(currentUser)) { + throw new Error('You are not authorized to delete an admin account'); + } + + if (!userToDelete._id) { return null; } await AccountModelType.destroy({ where: { - _id, + _id: userToDelete._id, }, }); - AppLogger.info(`[AccountProvider - deleteAccount] deleted account: ${_id}`); + AppLogger.info(`[AccountProvider - deleteAccount] deleted account: ${userToDelete._id}`); return { - _id, + _id: userToDelete._id, }; } catch (error) { AppLogger.info(`[AccountProvider - deleteAccount] error: ${error}`); @@ -172,6 +264,7 @@ const getAccountDetailsByParams = async ({ _id, email }: { _id?: number; email?: AppLogger.info( `[AccountProvider - getAccountDetailsByParams] account found, accountDetails: ${accountDetails._id}`, + `[AccountProvider - getAccountDetailsByParams] accountDetails: ${accountDetails.dataValues._id}`, ); return accountDetails.dataValues; @@ -226,6 +319,7 @@ const getAccountListByPageAndParams = async ({ const AccountProvider = { createAccount, editAccount, + updateAccountPassword, deleteAccount, getAccountDetailsByParams, getAccountListByPageAndParams, diff --git a/src/v6y-commons/src/types/AccountType.ts b/src/v6y-commons/src/types/AccountType.ts index 08eeb84b..0aeb1de4 100644 --- a/src/v6y-commons/src/types/AccountType.ts +++ b/src/v6y-commons/src/types/AccountType.ts @@ -11,11 +11,16 @@ export interface AccountInputType { _id?: number; username: string; email: string; - password: string; + password?: string; role: string; applications?: number[]; } +export interface AccountUpdatePasswordType { + _id: number; + password: string; +} + export interface AccountLoginType { email: string; password: string; diff --git a/src/v6y-commons/src/types/SearchQueryType.ts b/src/v6y-commons/src/types/SearchQueryType.ts index 1acef6d2..9281d003 100644 --- a/src/v6y-commons/src/types/SearchQueryType.ts +++ b/src/v6y-commons/src/types/SearchQueryType.ts @@ -5,5 +5,5 @@ export interface SearchQueryType { start?: number; limit?: number; where?: { _id: number; id?: string }; - sort?: string; + sort?: string | string[]; } diff --git a/src/v6y-front-bo/public/locales/en/common.json b/src/v6y-front-bo/public/locales/en/common.json index 844665ea..7c960fa3 100644 --- a/src/v6y-front-bo/public/locales/en/common.json +++ b/src/v6y-front-bo/public/locales/en/common.json @@ -58,6 +58,43 @@ "submit": "Update" } }, + "createAccount" : { + "title": "Create an account", + "fields": { + "account-infos-group": "Account Information", + "account-email" : { + "label": "Account email", + "placeholder": "Enter an account email", + "error": "Account email is a mandatory field!" + }, + "account-username" : { + "label": "Account username", + "placeholder": "Enter an account username", + "error": "Account username is a mandatory field!" + }, + "account-role" : { + "label": "Account role", + "placeholder": "Select an account role", + "options": { + "admin": "Administrator", + "user": "User" + }, + "error": "Account role is a mandatory field!" + }, + "account-password" : { + "label": "Account password", + "placeholder": "Enter your account password", + "error": "Account password is a mandatory field!" + }, + "applications-group": "Applications", + "account-applications" : { + "label": "Account applications", + "placeholder": "Select account applications", + "error": "You need to select at least one application!" + } + + } + }, "error": { "info": "You may have forgotten to add the {{action}} component to {{resource}} resource.", "404": "Sorry, the page you visited does not exist.", @@ -222,6 +259,29 @@ "show": "Show Notification" } }, + "v6y-accounts": { + "v6y-accounts":"Accounts", + "titles" : { + "list": "Accounts", + "create": "Create Account", + "edit": "Edit Account", + "show": "Show Account" + }, + "fields": { + "account-username":{ + "label": "Username" + }, + "account-email":{ + "label": "Email" + }, + "account-role":{ + "label": "Role" + }, + "account-applications":{ + "label": "Applications" + } + } + }, "v6y-faqs": { "v6y-faqs": "FAQ", "fields": { diff --git a/src/v6y-front-bo/public/locales/fr/common.json b/src/v6y-front-bo/public/locales/fr/common.json index 844665ea..7c960fa3 100644 --- a/src/v6y-front-bo/public/locales/fr/common.json +++ b/src/v6y-front-bo/public/locales/fr/common.json @@ -58,6 +58,43 @@ "submit": "Update" } }, + "createAccount" : { + "title": "Create an account", + "fields": { + "account-infos-group": "Account Information", + "account-email" : { + "label": "Account email", + "placeholder": "Enter an account email", + "error": "Account email is a mandatory field!" + }, + "account-username" : { + "label": "Account username", + "placeholder": "Enter an account username", + "error": "Account username is a mandatory field!" + }, + "account-role" : { + "label": "Account role", + "placeholder": "Select an account role", + "options": { + "admin": "Administrator", + "user": "User" + }, + "error": "Account role is a mandatory field!" + }, + "account-password" : { + "label": "Account password", + "placeholder": "Enter your account password", + "error": "Account password is a mandatory field!" + }, + "applications-group": "Applications", + "account-applications" : { + "label": "Account applications", + "placeholder": "Select account applications", + "error": "You need to select at least one application!" + } + + } + }, "error": { "info": "You may have forgotten to add the {{action}} component to {{resource}} resource.", "404": "Sorry, the page you visited does not exist.", @@ -222,6 +259,29 @@ "show": "Show Notification" } }, + "v6y-accounts": { + "v6y-accounts":"Accounts", + "titles" : { + "list": "Accounts", + "create": "Create Account", + "edit": "Edit Account", + "show": "Show Account" + }, + "fields": { + "account-username":{ + "label": "Username" + }, + "account-email":{ + "label": "Email" + }, + "account-role":{ + "label": "Role" + }, + "account-applications":{ + "label": "Applications" + } + } + }, "v6y-faqs": { "v6y-faqs": "FAQ", "fields": { diff --git a/src/v6y-front-bo/src/app/login/page.tsx b/src/v6y-front-bo/src/app/login/page.tsx index b6f618ff..2d6f6c48 100644 --- a/src/v6y-front-bo/src/app/login/page.tsx +++ b/src/v6y-front-bo/src/app/login/page.tsx @@ -1,7 +1,7 @@ import { redirect } from 'next/navigation'; import * as React from 'react'; -import { VitalityAuthLoginView } from '../../features/v6y-auth/VitalityAuthLoginView'; +import { VitalityAuthLoginView } from '../../features/v6y-auth/components/VitalityAuthLoginView'; import { AuthServerProvider } from '../../infrastructure/providers/AuthServerProvider'; export default async function Login() { diff --git a/src/v6y-front-bo/src/app/register/page.tsx b/src/v6y-front-bo/src/app/register/page.tsx deleted file mode 100644 index 0b73208e..00000000 --- a/src/v6y-front-bo/src/app/register/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { redirect } from 'next/navigation'; -import * as React from 'react'; - -import { VitalityAuthRegisterView } from '../../features/v6y-auth/VitalityAuthRegisterView'; -import { AuthServerProvider } from '../../infrastructure/providers/AuthServerProvider'; - -export default async function Register() { - const data = await getData(); - - if (data.authenticated) { - redirect(data?.redirectTo || '/'); - } - - return ; -} - -async function getData() { - const { authenticated, redirectTo, error } = await AuthServerProvider.check(); - - return { - authenticated, - redirectTo, - error, - }; -} diff --git a/src/v6y-front-bo/src/app/update-password/page.tsx b/src/v6y-front-bo/src/app/update-password/page.tsx index 22fac5b2..4c891e46 100644 --- a/src/v6y-front-bo/src/app/update-password/page.tsx +++ b/src/v6y-front-bo/src/app/update-password/page.tsx @@ -7,8 +7,8 @@ import { AuthServerProvider } from '../../infrastructure/providers/AuthServerPro export default async function UpdatePassword() { const data = await getData(); - if (data.authenticated) { - redirect(data?.redirectTo || '/'); + if (!data.authenticated) { + redirect(data?.redirectTo || '/login'); } return ; diff --git a/src/v6y-front-bo/src/app/v6y-accounts/create/page.tsx b/src/v6y-front-bo/src/app/v6y-accounts/create/page.tsx new file mode 100644 index 00000000..dd02e679 --- /dev/null +++ b/src/v6y-front-bo/src/app/v6y-accounts/create/page.tsx @@ -0,0 +1,9 @@ +'use client'; + +import * as React from 'react'; + +import VitalityAccountCreateView from '../../../features/v6y-accounts/components/VitalityAccountCreateView'; + +export default function VitalityAccountCreatePage() { + return ; +} diff --git a/src/v6y-front-bo/src/app/v6y-accounts/edit/[id]/page.tsx b/src/v6y-front-bo/src/app/v6y-accounts/edit/[id]/page.tsx new file mode 100644 index 00000000..1073b664 --- /dev/null +++ b/src/v6y-front-bo/src/app/v6y-accounts/edit/[id]/page.tsx @@ -0,0 +1,9 @@ +'use client'; + +import * as React from 'react'; + +import VitalityAccountEditView from '../../../../features/v6y-accounts/components/VitalityAccountEditView'; + +export default function VitalityAccountEditPage() { + return ; +} diff --git a/src/v6y-front-bo/src/app/v6y-accounts/layout.tsx b/src/v6y-front-bo/src/app/v6y-accounts/layout.tsx new file mode 100644 index 00000000..7cd9dc7f --- /dev/null +++ b/src/v6y-front-bo/src/app/v6y-accounts/layout.tsx @@ -0,0 +1,23 @@ +import { redirect } from 'next/navigation'; +import { ReactNode } from 'react'; + +import { AuthServerProvider } from '../../infrastructure/providers/AuthServerProvider'; + +export default async function Layout({ children }: { children: ReactNode }) { + const data = await getData(); + + if (!data.authenticated) { + return redirect(data?.redirectTo || '/login'); + } + + return children; +} + +async function getData() { + const { authenticated, redirectTo } = await AuthServerProvider.check(); + + return { + authenticated, + redirectTo, + }; +} diff --git a/src/v6y-front-bo/src/app/v6y-accounts/page.tsx b/src/v6y-front-bo/src/app/v6y-accounts/page.tsx new file mode 100644 index 00000000..faf62a55 --- /dev/null +++ b/src/v6y-front-bo/src/app/v6y-accounts/page.tsx @@ -0,0 +1,9 @@ +'use client'; + +import * as React from 'react'; + +import VitalityAccountListView from '../../features/v6y-accounts/components/VitalityAccountListView'; + +export default function AccountList() { + return ; +} diff --git a/src/v6y-front-bo/src/app/v6y-accounts/show/[id]/page.tsx b/src/v6y-front-bo/src/app/v6y-accounts/show/[id]/page.tsx new file mode 100644 index 00000000..e9f5cd93 --- /dev/null +++ b/src/v6y-front-bo/src/app/v6y-accounts/show/[id]/page.tsx @@ -0,0 +1,9 @@ +'use client'; + +import * as React from 'react'; + +import VitalityAccountDetailsView from '../../../../features/v6y-accounts/components/VitalityAccountDetailsView'; + +export default function VitalityAccountDetailsPage() { + return ; +} diff --git a/src/v6y-front-bo/src/features/v6y-applications/apis/getApplicationList.ts b/src/v6y-front-bo/src/commons/apis/getApplicationList.ts similarity index 56% rename from src/v6y-front-bo/src/features/v6y-applications/apis/getApplicationList.ts rename to src/v6y-front-bo/src/commons/apis/getApplicationList.ts index b0f83bcc..1a1a8d82 100644 --- a/src/v6y-front-bo/src/features/v6y-applications/apis/getApplicationList.ts +++ b/src/v6y-front-bo/src/commons/apis/getApplicationList.ts @@ -1,8 +1,8 @@ import { gql } from 'graphql-request'; const GetApplicationList = gql` - query GetApplicationList($start: Int, $limit: Int, $sort: String) { - getApplicationListByPageAndParams(start: $start, limit: $limit, sort: $sort) { + query GetApplicationList($sort: [String]) { + getApplicationList(sort: $sort) { _id acronym name diff --git a/src/v6y-front-bo/src/commons/components/VitalityFormFieldSet.tsx b/src/v6y-front-bo/src/commons/components/VitalityFormFieldSet.tsx index 547aa875..5dddce23 100644 --- a/src/v6y-front-bo/src/commons/components/VitalityFormFieldSet.tsx +++ b/src/v6y-front-bo/src/commons/components/VitalityFormFieldSet.tsx @@ -17,12 +17,15 @@ const VitalityFormFieldSet = ({ groupTitle, items, selectOptions }: VitalityForm label={item.label} name={item.name} rules={item.rules} + initialValue={item.defaultValue} > {item.type === 'select' && ( )} diff --git a/src/v6y-front-bo/src/commons/config/VitalityDetailsConfig.tsx b/src/v6y-front-bo/src/commons/config/VitalityDetailsConfig.tsx index 73a1a918..a8a61543 100644 --- a/src/v6y-front-bo/src/commons/config/VitalityDetailsConfig.tsx +++ b/src/v6y-front-bo/src/commons/config/VitalityDetailsConfig.tsx @@ -1,4 +1,5 @@ import { + AccountType, ApplicationType, AuditHelpType, DependencyStatusHelpType, @@ -13,6 +14,23 @@ import { ReactNode } from 'react'; import { TranslateType } from '../../infrastructure/types/TranslationType'; import VitalityLinks from '../components/VitalityLinks'; +export const formatAccountDetails = ( + translate: TranslateType, + details: AccountType, +): Record => { + if (!Object.keys(details || {})?.length) { + return {}; + } + + return { + [translate('v6y-accounts.fields.account-username.label') || '']: details.username, + [translate('v6y-accounts.fields.account-email.label') || '']: details.email, + [translate('v6y-accounts.fields.account-role.label') || '']: details.role, + [translate('v6y-accounts.fields.account-applications.label') || '']: + details.applications?.join(', '), + }; +}; + export const formatApplicationDetails = ( translate: TranslateType, details: ApplicationType, diff --git a/src/v6y-front-bo/src/commons/config/VitalityFormConfig.tsx b/src/v6y-front-bo/src/commons/config/VitalityFormConfig.tsx index 09d4ea91..4f31a31d 100644 --- a/src/v6y-front-bo/src/commons/config/VitalityFormConfig.tsx +++ b/src/v6y-front-bo/src/commons/config/VitalityFormConfig.tsx @@ -754,3 +754,160 @@ export const deprecatedDependencyCreateOrEditFormOutputAdapter = ( name: params?.['deprecated-dependency-name'], }, }); + +export const accountCreateOrEditFormInAdapter = (params: Record) => ({ + _id: params?._id, + 'account-email': params?.['email'], + 'account-username': params?.['username'], + 'account-role': params?.['role'], + 'account-password': params?.['password'], + 'account-applications': params?.['applications'], +}); + +export const accountCreateOrEditFormOutputAdapter = (params: Record) => ({ + accountInput: { + _id: params?.['_id'], + email: params?.['account-email'], + username: params?.['account-username'], + role: params?.['account-role'], + password: params?.['account-password'], + applications: params?.['account-applications'], + }, +}); + +export const accountCreateEditItems = ( + translate: TranslateType, + role: string, + applications: ApplicationType[], + edit: boolean = false, +) => { + const applicationsValues = applications?.map((application) => ({ + value: application._id, + label: application.name, + })); + + const roles = [ + { + label: translate('pages.createAccount.fields.account-role.options.admin'), + value: 'ADMIN', + }, + { label: translate('pages.createAccount.fields.account-role.options.user'), value: 'USER' }, + ]; + + return [ + , + , + ]; +}; + +export const accountInfosFormItems = (translate: TranslateType, role: string, edit: boolean) => { + return [ + { + id: 'account-email', + name: 'account-email', + label: translate('pages.createAccount.fields.account-email.label'), + placeholder: translate('pages.createAccount.fields.account-email.placeholder'), + rules: [ + { + required: true, + message: translate('pages.createAccount.fields.account-email.error'), + }, + { + type: 'email', + message: translate('pages.createAccount.fields.account-email.error'), + }, + ], + }, + { + id: 'account-username', + name: 'account-username', + label: translate('pages.createAccount.fields.account-username.label'), + placeholder: translate('pages.createAccount.fields.account-username.placeholder'), + rules: [ + { + required: true, + message: translate('pages.createAccount.fields.account-username.error'), + }, + ], + }, + { + id: 'account-role', + name: 'account-role', + label: translate('pages.createAccount.fields.account-role.label'), + placeholder: translate('pages.createAccount.fields.account-role.placeholder'), + type: 'select', + disabled: role !== 'SUPERADMIN', + defaultValue: role !== 'SUPERADMIN' ? 'USER' : undefined, + rules: [ + { + required: true, + message: translate('pages.createAccount.fields.account-role.error'), + }, + ], + options: [ + { + label: translate('pages.createAccount.fields.account-role.options.admin'), + value: 'ADMIN', + }, + { + label: translate('pages.createAccount.fields.account-role.options.user'), + value: 'USER', + }, + ], + }, + { + id: 'account-password', + name: 'account-password', + type: 'password', + label: translate('pages.createAccount.fields.account-password.label'), + placeholder: translate('pages.createAccount.fields.account-password.placeholder'), + rules: !edit + ? [ + { + required: true, + message: translate('pages.createAccount.fields.account-password.error'), + }, + ] + : [], + }, + ]; +}; + +export const accountApplicationsFormItems = (translate: TranslateType) => { + return [ + { + id: 'account-applications', + name: 'account-applications', + label: translate('pages.createAccount.fields.account-applications.label'), + placeholder: translate('pages.createAccount.fields.account-applications.placeholder'), + type: 'select', + mode: 'multiple', + rules: [ + { + required: true, + message: translate('pages.createAccount.fields.account-applications.error'), + validator: (_: unknown, value: string[]) => + value && value.length > 0 + ? Promise.resolve() + : Promise.reject( + new Error( + translate( + 'pages.createAccount.fields.account-applications.error', + ), + ), + ), + }, + ], + }, + ]; +}; diff --git a/src/v6y-front-bo/src/commons/config/VitalityNavigationConfig.ts b/src/v6y-front-bo/src/commons/config/VitalityNavigationConfig.ts index ff682ac8..b06712f6 100644 --- a/src/v6y-front-bo/src/commons/config/VitalityNavigationConfig.ts +++ b/src/v6y-front-bo/src/commons/config/VitalityNavigationConfig.ts @@ -1,4 +1,14 @@ export const VitalityRoutes = [ + { + name: 'v6y-accounts', + list: '/v6y-accounts', + create: '/v6y-accounts/create', + edit: '/v6y-accounts/edit/:id', + show: '/v6y-accounts/show/:id', + meta: { + canDelete: true, + }, + }, { name: 'v6y-applications', list: '/v6y-applications', diff --git a/src/v6y-front-bo/src/commons/hooks/useRole.ts b/src/v6y-front-bo/src/commons/hooks/useRole.ts new file mode 100644 index 00000000..50abae1b --- /dev/null +++ b/src/v6y-front-bo/src/commons/hooks/useRole.ts @@ -0,0 +1,12 @@ +import Cookies from 'js-cookie'; + +export const useRole = () => { + const getRole = () => { + const auth = Cookies.get('auth'); + return JSON.parse(auth || '{}')?.role; + }; + + return { + getRole, + }; +}; diff --git a/src/v6y-front-bo/src/commons/hooks/useToken.ts b/src/v6y-front-bo/src/commons/hooks/useToken.ts new file mode 100644 index 00000000..5f6cec7a --- /dev/null +++ b/src/v6y-front-bo/src/commons/hooks/useToken.ts @@ -0,0 +1,10 @@ +import Cookies from 'js-cookie'; + +const useToken = () => { + const auth = Cookies.get('auth'); + const token = JSON.parse(auth || '{}')?.token; + + return token; +}; + +export default useToken; diff --git a/src/v6y-front-bo/src/features/v6y-accounts/apis/createOrEditAccount.ts b/src/v6y-front-bo/src/features/v6y-accounts/apis/createOrEditAccount.ts new file mode 100644 index 00000000..9aad4a40 --- /dev/null +++ b/src/v6y-front-bo/src/features/v6y-accounts/apis/createOrEditAccount.ts @@ -0,0 +1,11 @@ +import { gql } from 'graphql-request'; + +const CreateOrEditAccount = gql` + mutation CreateOrEditAccount($accountInput: AccountCreateOrEditInput!) { + createOrEditAccount(input: $accountInput) { + _id + } + } +`; + +export default CreateOrEditAccount; diff --git a/src/v6y-front-bo/src/features/v6y-accounts/apis/deleteAccount.ts b/src/v6y-front-bo/src/features/v6y-accounts/apis/deleteAccount.ts new file mode 100644 index 00000000..5f26643e --- /dev/null +++ b/src/v6y-front-bo/src/features/v6y-accounts/apis/deleteAccount.ts @@ -0,0 +1,11 @@ +import { gql } from 'graphql-request'; + +const DeleteAccount = gql` + mutation DeleteAccount($input: AccountDeleteInput!) { + deleteAccount(input: $input) { + _id + } + } +`; + +export default DeleteAccount; diff --git a/src/v6y-front-bo/src/features/v6y-accounts/apis/getAccountDetailsByParams.ts b/src/v6y-front-bo/src/features/v6y-accounts/apis/getAccountDetailsByParams.ts new file mode 100644 index 00000000..9d7eb2a5 --- /dev/null +++ b/src/v6y-front-bo/src/features/v6y-accounts/apis/getAccountDetailsByParams.ts @@ -0,0 +1,15 @@ +import { gql } from 'graphql-request'; + +const GetAccountDetailsByParams = gql` + query GetAccountDetailsByParams($_id: Int!) { + getAccountDetailsByParams(_id: $_id) { + _id + username + email + role + applications + } + } +`; + +export default GetAccountDetailsByParams; diff --git a/src/v6y-front-bo/src/features/v6y-accounts/apis/getAccountListByPageAndParams.ts b/src/v6y-front-bo/src/features/v6y-accounts/apis/getAccountListByPageAndParams.ts new file mode 100644 index 00000000..28e06cd5 --- /dev/null +++ b/src/v6y-front-bo/src/features/v6y-accounts/apis/getAccountListByPageAndParams.ts @@ -0,0 +1,14 @@ +import { gql } from 'graphql-request'; + +const GetAccountListByPageAndParams = gql` + query GetAccountListByPageAndParams { + getAccountListByPageAndParams { + _id + username + email + role + } + } +`; + +export default GetAccountListByPageAndParams; diff --git a/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountCreateView.tsx b/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountCreateView.tsx new file mode 100644 index 00000000..7c2cc33b --- /dev/null +++ b/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountCreateView.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { ApplicationType } from '@v6y/commons/src/types/ApplicationType'; +import { Typography } from 'antd'; +import { useEffect, useState } from 'react'; + +import GetApplicationList from '../../../commons/apis/getApplicationList'; +import VitalityEmptyView from '../../../commons/components/VitalityEmptyView'; +import { + accountCreateEditItems, + accountCreateOrEditFormOutputAdapter, +} from '../../../commons/config/VitalityFormConfig'; +import { useRole } from '../../../commons/hooks/useRole'; +import { useTranslation } from '../../../infrastructure/adapters/translation/TranslationAdapter'; +import RefineSelectWrapper from '../../../infrastructure/components/RefineSelectWrapper'; +import CreateOrEditAccount from '../apis/createOrEditAccount'; + +export default function VitalityAccountCreateView() { + const { translate } = useTranslation(); + const [userRole, setUserRole] = useState(null); + const { getRole } = useRole(); + + useEffect(() => { + setUserRole(getRole()); + }, [getRole]); + + if (!userRole) { + return ; + } + + return ( + + {translate('v6y-accounts.titles.create')} + + } + createOptions={{ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + createResource: 'createOrEditAccount', + createFormAdapter: accountCreateOrEditFormOutputAdapter, + createQuery: CreateOrEditAccount, + createQueryParams: {}, + }} + selectOptions={{ + resource: 'getApplicationList', + query: GetApplicationList, + }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + renderSelectOption={(applications: ApplicationType[]) => { + return accountCreateEditItems(translate, userRole, applications); + }} + /> + ); +} diff --git a/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountDetailsView.tsx b/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountDetailsView.tsx new file mode 100644 index 00000000..f85083fb --- /dev/null +++ b/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountDetailsView.tsx @@ -0,0 +1,60 @@ +import { HttpError, useParsed } from '@refinedev/core'; +import { AccountType } from '@v6y/commons'; +import { Typography } from 'antd'; +import * as React from 'react'; + +import VitalityDetailsView from '../../../commons/components/VitalityDetailsView'; +import { formatAccountDetails } from '../../../commons/config/VitalityDetailsConfig'; +import { useTranslation } from '../../../infrastructure/adapters/translation/TranslationAdapter'; +import RefineShowWrapper from '../../../infrastructure/components/RefineShowWrapper'; +import Matcher from '../../../infrastructure/utils/Matcher'; +import GetAccountDetailsByParams from '../apis/getAccountDetailsByParams'; + +export default function VitalityAccountDetailsView() { + const { translate } = useTranslation(); + + const { id } = useParsed(); + + const renderShowView: ({ + data, + error, + }: { + data?: unknown; + error: HttpError | string | undefined; + }) => React.JSX.Element = ({ data, error }) => { + const errorMessage = Matcher() + .with( + () => (error as HttpError)?.message?.length > 0, + () => (error as HttpError)?.message, + ) + .with( + () => typeof error === 'string', + () => error, + ) + .otherwise(() => ''); + return ( + + ); + }; + + return ( + + {translate('v6y-account.titles.show')} + + } + queryOptions={{ + resource: 'getAccountDetailsByParams', + query: GetAccountDetailsByParams, + queryParams: { + _id: parseInt(id as string, 10), + }, + }} + renderShowView={renderShowView} + /> + ); +} diff --git a/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountEditView.tsx b/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountEditView.tsx new file mode 100644 index 00000000..e31442bf --- /dev/null +++ b/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountEditView.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useParsed } from '@refinedev/core'; +import { ApplicationType } from '@v6y/commons/src/types/ApplicationType'; +import { Typography } from 'antd'; +import { useEffect, useState } from 'react'; +import React from 'react'; + +import GetApplicationList from '../../../commons/apis/getApplicationList'; +import VitalityEmptyView from '../../../commons/components/VitalityEmptyView'; +import { + accountCreateEditItems, + accountCreateOrEditFormInAdapter, + accountCreateOrEditFormOutputAdapter, +} from '../../../commons/config/VitalityFormConfig'; +import { useRole } from '../../../commons/hooks/useRole'; +import { useTranslation } from '../../../infrastructure/adapters/translation/TranslationAdapter'; +import RefineSelectWrapper from '../../../infrastructure/components/RefineSelectWrapper'; +import CreateOrEditAccount from '../apis/createOrEditAccount'; +import GetAccountDetailsByParams from '../apis/getAccountDetailsByParams'; + +export default function VitalityAccountEditView() { + const { translate } = useTranslation(); + const [userRole, setUserRole] = useState(null); + const { getRole } = useRole(); + const { id } = useParsed(); + + useEffect(() => { + setUserRole(getRole()); + }, [getRole]); + + if (!userRole) { + return ; + } + + return ( + + {translate('v6y-accounts.titles.edit')} + + } + queryOptions={{ + queryFormAdapter: accountCreateOrEditFormInAdapter, + query: GetAccountDetailsByParams, + queryResource: 'getAccountDetailsByParams', + queryParams: { + _id: parseInt(id as string, 10), + }, + }} + mutationOptions={{ + editResource: 'createOrEditAccount', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + editFormAdapter: accountCreateOrEditFormOutputAdapter, + editQuery: CreateOrEditAccount, + editQueryParams: { + _id: parseInt(id as string, 10), + }, + }} + selectOptions={{ + resource: 'getApplicationList', + query: GetApplicationList, + }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + renderSelectOption={(applications: ApplicationType[]) => { + return accountCreateEditItems(translate, userRole, applications, true); + }} + /> + ); +} diff --git a/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountListView.tsx b/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountListView.tsx new file mode 100644 index 00000000..936e5db2 --- /dev/null +++ b/src/v6y-front-bo/src/features/v6y-accounts/components/VitalityAccountListView.tsx @@ -0,0 +1,44 @@ +import VitalityTable from '../../../commons/components/VitalityTable'; +import { + buildCommonTableColumns, + buildCommonTableDataSource, +} from '../../../commons/config/VitalityTableConfig'; +import { useTranslation } from '../../../infrastructure/adapters/translation/TranslationAdapter'; +import RefineTableWrapper from '../../../infrastructure/components/RefineTableWrapper'; +import DeleteAccount from '../apis/deleteAccount'; +import GetAccountListByPageAndParams from '../apis/getAccountListByPageAndParams'; + +export default function VitalityAccountListView() { + const { translate } = useTranslation(); + + return ( + ( + + )} + /> + ); +} diff --git a/src/v6y-front-bo/src/features/v6y-applications/components/VitalityApplicationListView.tsx b/src/v6y-front-bo/src/features/v6y-applications/components/VitalityApplicationListView.tsx index 1b824321..02d7c832 100644 --- a/src/v6y-front-bo/src/features/v6y-applications/components/VitalityApplicationListView.tsx +++ b/src/v6y-front-bo/src/features/v6y-applications/components/VitalityApplicationListView.tsx @@ -1,5 +1,6 @@ import { ApplicationType } from '@v6y/commons'; +import GetApplicationList from '../../../commons/apis/getApplicationList'; import VitalityTable from '../../../commons/components/VitalityTable'; import { buildCommonTableColumns, @@ -8,7 +9,6 @@ import { import { useTranslation } from '../../../infrastructure/adapters/translation/TranslationAdapter'; import RefineTableWrapper from '../../../infrastructure/components/RefineTableWrapper'; import DeleteApplication from '../apis/deleteApplication'; -import GetApplicationList from '../apis/getApplicationList'; export default function VitalityApplicationListView() { const { translate } = useTranslation(); diff --git a/src/v6y-front-bo/src/features/v6y-auth/VitalityAuthRegisterView.tsx b/src/v6y-front-bo/src/features/v6y-auth/VitalityAuthRegisterView.tsx deleted file mode 100644 index 77b03c2a..00000000 --- a/src/v6y-front-bo/src/features/v6y-auth/VitalityAuthRegisterView.tsx +++ /dev/null @@ -1,21 +0,0 @@ -'use client'; - -import { AuthPage as AuthPageBase } from '@refinedev/antd'; -import { Typography } from 'antd'; - -import { useTranslation } from '../../infrastructure/adapters/translation/TranslationAdapter'; - -export const VitalityAuthRegisterView = () => { - const { translate } = useTranslation(); - - return ( - - {translate('v6y-authentication.title')} - - } - /> - ); -}; diff --git a/src/v6y-front-bo/src/features/v6y-auth/VitalityAuthLoginView.tsx b/src/v6y-front-bo/src/features/v6y-auth/components/VitalityAuthLoginView.tsx similarity index 90% rename from src/v6y-front-bo/src/features/v6y-auth/VitalityAuthLoginView.tsx rename to src/v6y-front-bo/src/features/v6y-auth/components/VitalityAuthLoginView.tsx index 738d2692..1071de76 100644 --- a/src/v6y-front-bo/src/features/v6y-auth/VitalityAuthLoginView.tsx +++ b/src/v6y-front-bo/src/features/v6y-auth/components/VitalityAuthLoginView.tsx @@ -3,7 +3,7 @@ import { AuthPage as AuthPageBase } from '@refinedev/antd'; import { Checkbox, Form, Typography } from 'antd'; -import { useTranslation } from '../../infrastructure/adapters/translation/TranslationAdapter'; +import { useTranslation } from '../../../infrastructure/adapters/translation/TranslationAdapter'; export const VitalityAuthLoginView = () => { const { translate } = useTranslation(); diff --git a/src/v6y-front-bo/src/infrastructure/adapters/api/.gitkeep b/src/v6y-front-bo/src/infrastructure/adapters/api/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/v6y-front-bo/src/infrastructure/adapters/api/GraphQLClient.ts b/src/v6y-front-bo/src/infrastructure/adapters/api/GraphQLClient.ts new file mode 100644 index 00000000..5814da3e --- /dev/null +++ b/src/v6y-front-bo/src/infrastructure/adapters/api/GraphQLClient.ts @@ -0,0 +1,24 @@ +import { GraphQLClient } from 'graphql-request'; +import Cookies from 'js-cookie'; + +export const gqlClient = new GraphQLClient(process.env.NEXT_PUBLIC_GQL_API_BASE_PATH as string, { + fetch: (url: RequestInfo | URL, options?: RequestInit) => { + return fetch(url, { + ...options, + headers: { + ...(options?.headers || {}), + Authorization: `Bearer ${JSON.parse(Cookies.get('auth') || '{}')?.token}`, + }, + }); + }, +}); + +type GqlClientRequestParams = { + gqlQueryPath?: string; + gqlQueryParams?: Record; +}; + +export const gqlClientRequest = ({ + gqlQueryPath, + gqlQueryParams, +}: GqlClientRequestParams): Promise => gqlClient.request(gqlQueryPath, gqlQueryParams); diff --git a/src/v6y-front-bo/src/infrastructure/components/RefineCreateWrapper.tsx b/src/v6y-front-bo/src/infrastructure/components/RefineCreateWrapper.tsx index e352149b..d55ac6ae 100644 --- a/src/v6y-front-bo/src/infrastructure/components/RefineCreateWrapper.tsx +++ b/src/v6y-front-bo/src/infrastructure/components/RefineCreateWrapper.tsx @@ -1,11 +1,20 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck 'use client'; import { Create, useForm } from '@refinedev/antd'; +import { BaseRecord, GetOneResponse } from '@refinedev/core'; import { Form } from 'antd'; -import GraphqlClientRequest from 'graphql-request'; +import { gqlClientRequest } from '../adapters/api/GraphQLClient'; import { FormCreateOptionsType } from '../types/FormType'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + export default function RefineCreateWrapper({ title, createOptions, @@ -14,17 +23,15 @@ export default function RefineCreateWrapper({ const { form, formProps, saveButtonProps } = useForm({ defaultFormValues: {}, createMutationOptions: { - mutationFn: async () => - GraphqlClientRequest( - process.env.NEXT_PUBLIC_GQL_API_BASE_PATH as string, - createOptions?.createQuery, - createOptions?.createFormAdapter?.({ - ...(createOptions?.createQueryParams || {}), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - ...(form?.getFieldsValue() || {}), - }) || {}, - ), + mutationFn: async (): Promise> => + gqlClientRequest({ + gqlQueryPath: createOptions?.createQuery, + gqlQueryParams: + createOptions?.createFormAdapter?.({ + ...(createOptions?.createQueryParams || {}), + ...(form?.getFieldsValue() || {}), + }) || {}, + }), }, }); diff --git a/src/v6y-front-bo/src/infrastructure/components/RefineEditWrapper.tsx b/src/v6y-front-bo/src/infrastructure/components/RefineEditWrapper.tsx index d9ef0f88..8055500a 100644 --- a/src/v6y-front-bo/src/infrastructure/components/RefineEditWrapper.tsx +++ b/src/v6y-front-bo/src/infrastructure/components/RefineEditWrapper.tsx @@ -1,13 +1,21 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck 'use client'; import { Edit, useForm } from '@refinedev/antd'; import { BaseRecord, GetOneResponse } from '@refinedev/core'; import { Form } from 'antd'; -import GraphqlClientRequest from 'graphql-request'; import { useEffect } from 'react'; +import { gqlClientRequest } from '../adapters/api/GraphQLClient'; import { FormWrapperProps } from '../types/FormType'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + export default function RefineEditWrapper({ title, queryOptions, @@ -16,29 +24,24 @@ export default function RefineEditWrapper({ }: FormWrapperProps) { const { form, formProps, saveButtonProps, query } = useForm({ queryOptions: { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error queryKey: [queryOptions?.resource, queryOptions?.queryParams], queryFn: async (): Promise> => - GraphqlClientRequest( - process.env.NEXT_PUBLIC_GQL_API_BASE_PATH as string, - queryOptions?.query as string, - queryOptions?.queryParams, - ), + gqlClientRequest({ + gqlQueryPath: queryOptions?.query, + gqlQueryParams: queryOptions?.queryParams, + }), }, updateMutationOptions: { mutationKey: [mutationOptions?.resource, mutationOptions?.editQuery], - mutationFn: async () => - GraphqlClientRequest( - process.env.NEXT_PUBLIC_GQL_API_BASE_PATH as string, - mutationOptions?.editQuery, - mutationOptions?.editFormAdapter?.({ - ...(mutationOptions?.editQueryParams || {}), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - ...(form?.getFieldsValue() || {}), - }) || {}, - ), + mutationFn: async (): Promise> => + gqlClientRequest({ + gqlQueryPath: mutationOptions?.editQuery, + gqlQueryParams: + mutationOptions?.editFormAdapter?.({ + ...(mutationOptions?.editQueryParams || {}), + ...(form?.getFieldsValue() || {}), + }) || {}, + }), }, }); @@ -47,8 +50,6 @@ export default function RefineEditWrapper({ queryOptions?.queryResource as keyof typeof query.data ] as Record | undefined; if (formDetails && Object.keys(formDetails).length) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error form?.setFieldsValue( queryOptions?.queryFormAdapter?.(formDetails as Record) || {}, ); diff --git a/src/v6y-front-bo/src/infrastructure/components/RefineSelectWrapper.tsx b/src/v6y-front-bo/src/infrastructure/components/RefineSelectWrapper.tsx index 4e28e3cb..a6b83dd6 100644 --- a/src/v6y-front-bo/src/infrastructure/components/RefineSelectWrapper.tsx +++ b/src/v6y-front-bo/src/infrastructure/components/RefineSelectWrapper.tsx @@ -1,62 +1,102 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck 'use client'; import { Edit, useForm, useSelect } from '@refinedev/antd'; import { BaseRecord, GetOneResponse } from '@refinedev/core'; import { Form } from 'antd'; -import GraphqlClientRequest from 'graphql-request'; import { ReactNode, useEffect } from 'react'; +import { gqlClientRequest } from '../adapters/api/GraphQLClient'; import { FormWrapperProps } from '../types/FormType'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + export default function RefineSelectWrapper({ title, queryOptions, mutationOptions, + createOptions, selectOptions, renderSelectOption, }: FormWrapperProps) { + const formQueryOptions = queryOptions + ? { + queryOptions: { + enabled: true, + queryKey: [queryOptions?.resource, queryOptions?.queryParams], + queryFn: async (): Promise> => + gqlClientRequest({ + gqlQueryPath: queryOptions?.query, + gqlQueryParams: queryOptions?.queryParams, + }), + }, + } + : {}; + + const formMutationOptions = mutationOptions + ? { + updateMutationOptions: { + mutationKey: [mutationOptions?.editResource, mutationOptions?.editQuery], + mutationFn: async (): Promise> => { + const { editQuery, editFormAdapter, editQueryParams } = mutationOptions; + return gqlClientRequest({ + gqlQueryPath: editQuery, + gqlQueryParams: + editFormAdapter?.({ + ...(editQueryParams || {}), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + ...(form?.getFieldsValue() || {}), + }) || {}, + }); + }, + }, + } + : {}; + + const formCreateOptions = createOptions + ? { + createMutationOptions: { + mutationKey: [createOptions?.createResource, createOptions?.createQuery], + mutationFn: async (): Promise> => { + const { createQuery, createFormAdapter, createQueryParams } = createOptions; + return gqlClientRequest({ + gqlQueryPath: createQuery, + gqlQueryParams: + createFormAdapter?.({ + ...(createQueryParams || {}), + ...(form?.getFieldsValue() || {}), + }) || {}, + }); + }, + }, + } + : {}; + const { form, formProps, saveButtonProps, query } = useForm({ + ...formQueryOptions, + ...formMutationOptions, + ...formCreateOptions, + defaultFormValues: {}, + }); + + const { query: selectQueryResult } = useSelect({ queryOptions: { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error enabled: true, - queryKey: [queryOptions?.resource, queryOptions?.queryParams], + queryKey: [selectOptions?.resource, selectOptions?.queryParams], queryFn: async (): Promise> => - GraphqlClientRequest( - process.env.NEXT_PUBLIC_GQL_API_BASE_PATH || '', - queryOptions?.query, - queryOptions?.queryParams, - ), - }, - updateMutationOptions: { - mutationKey: ['update', mutationOptions?.editQuery], - mutationFn: async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const { editQuery, editFormAdapter, editQueryParams } = mutationOptions; - return GraphqlClientRequest( - process.env.NEXT_PUBLIC_GQL_API_BASE_PATH || '', - editQuery, - editFormAdapter?.({ - ...(editQueryParams || {}), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - ...(form?.getFieldsValue() || {}), - }) || {}, - ); - }, + gqlClientRequest({ + gqlQueryPath: selectOptions?.query, + gqlQueryParams: selectOptions?.queryParams, + }), }, }); - const { query: selectQueryResult } = useSelect({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - resource: selectOptions?.resource, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - meta: { gqlQuery: selectOptions?.query }, - }); - useEffect(() => { const formDetails = query?.data?.[queryOptions?.queryResource]; if (Object.keys(formDetails || {})?.length) { @@ -66,15 +106,17 @@ export default function RefineSelectWrapper({ } }, [form, query?.data, queryOptions]); + const isLoading = selectQueryResult?.isLoading || (queryOptions && query?.isLoading); + return (
- {renderSelectOption?.(selectQueryResult?.data?.data)?.map( + {renderSelectOption?.(selectQueryResult?.data?.[selectOptions?.resource])?.map( (item: ReactNode) => item, )}
diff --git a/src/v6y-front-bo/src/infrastructure/components/RefineShowWrapper.tsx b/src/v6y-front-bo/src/infrastructure/components/RefineShowWrapper.tsx index 41d87daa..a4aded44 100644 --- a/src/v6y-front-bo/src/infrastructure/components/RefineShowWrapper.tsx +++ b/src/v6y-front-bo/src/infrastructure/components/RefineShowWrapper.tsx @@ -1,11 +1,19 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck 'use client'; import { RefreshButton, Show } from '@refinedev/antd'; import { BaseRecord, GetOneResponse, useShow } from '@refinedev/core'; -import GraphqlClientRequest from 'graphql-request'; +import { gqlClientRequest } from '../adapters/api/GraphQLClient'; import { FormShowOptions } from '../types/FormType'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + export default function RefineShowWrapper({ title, renderShowView, @@ -13,16 +21,13 @@ export default function RefineShowWrapper({ }: FormShowOptions) { const { query } = useShow({ queryOptions: { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error enabled: queryOptions?.enabled || true, queryKey: [queryOptions?.resource, queryOptions?.queryParams], queryFn: async (): Promise> => - GraphqlClientRequest( - process.env.NEXT_PUBLIC_GQL_API_BASE_PATH as string, - queryOptions?.query as string, - queryOptions?.queryParams, - ), + gqlClientRequest({ + gqlQueryPath: queryOptions?.query, + gqlQueryParams: queryOptions?.queryParams, + }), }, }); diff --git a/src/v6y-front-bo/src/infrastructure/components/RefineTableWrapper.tsx b/src/v6y-front-bo/src/infrastructure/components/RefineTableWrapper.tsx index e0a3b9db..d9a37d7d 100644 --- a/src/v6y-front-bo/src/infrastructure/components/RefineTableWrapper.tsx +++ b/src/v6y-front-bo/src/infrastructure/components/RefineTableWrapper.tsx @@ -1,3 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck import { List, RefreshButton, useTable } from '@refinedev/antd'; import { Typography } from 'antd'; import { ReactNode } from 'react'; @@ -14,8 +16,6 @@ export default function RefineTableWrapper({ }: RefineTableType) { const { tableProps, tableQuery } = useTable({ resource: queryOptions?.resource, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error meta: { gqlQuery: queryOptions?.query }, initialSorter: defaultSorter, }); diff --git a/src/v6y-front-bo/src/infrastructure/providers/GraphQLProvider.ts b/src/v6y-front-bo/src/infrastructure/providers/GraphQLProvider.ts index 4692fb3c..a148a5c8 100644 --- a/src/v6y-front-bo/src/infrastructure/providers/GraphQLProvider.ts +++ b/src/v6y-front-bo/src/infrastructure/providers/GraphQLProvider.ts @@ -1,23 +1,12 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck import type { AuthProvider } from '@refinedev/core'; -import dataProvider, { GraphQLClient, graphqlWS, liveProvider } from '@refinedev/graphql'; +import dataProvider, { graphqlWS, liveProvider } from '@refinedev/graphql'; import Cookies from 'js-cookie'; -const dataClient = new GraphQLClient(process.env.NEXT_PUBLIC_GQL_API_BASE_PATH as string, { - fetch: (url: RequestInfo | URL, options?: RequestInit) => { - return fetch(url, { - ...options, - headers: { - ...(options?.headers || {}), - /** - * For demo purposes, we're using `localStorage` to access the token. - * You can use your own authentication logic here. - * In real world applications, you'll need to handle it in sync with your `authProvider`. - */ - Authorization: `Bearer ${JSON.parse(Cookies.get('auth') || '')?.email}`, - }, - }); - }, -}); +import { gqlClient } from '../adapters/api/GraphQLClient'; + +const dataClient = gqlClient; const wsClient = graphqlWS.createClient({ url: process.env.NEXT_PUBLIC_GQL_API_BASE_PATH as string, @@ -27,76 +16,98 @@ export const gqlDataProvider = dataProvider(dataClient); export const gqlLiveProvider = liveProvider(wsClient); -const mockUsers = [ - { - email: 'admin@refine.dev', - name: 'John Doe', - avatar: 'https://i.pravatar.cc/150?img=1', - roles: ['admin'], - }, - { - email: 'editor@refine.dev', - name: 'Jane Doe', - avatar: 'https://i.pravatar.cc/150?img=1', - roles: ['editor'], - }, - { - email: 'demo@refine.dev', - name: 'Jane Doe', - avatar: 'https://i.pravatar.cc/150?img=1', - roles: ['user'], - }, -]; - export const gqlAuthProvider: AuthProvider = { - login: async ({ email /*username, password, remember*/ }) => { - // Suppose we actually send a request to the back end here. - const user = mockUsers.find((item) => item.email === email); + login: async ({ email, password }) => { + try { + const apiUrl = process.env.NEXT_PUBLIC_GQL_API_BASE_PATH; + if (!apiUrl) { + throw new Error('NEXT_PUBLIC_GQL_API_BASE_PATH is not defined'); + } - if (user) { - Cookies.set('auth', JSON.stringify(user), { - expires: 30, // 30 days - path: '/', + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + operationName: 'LoginAccount', + query: ` + query LoginAccount($input: AccountLoginInput!) { + loginAccount(input: $input) { + _id + role + token + } + } + `, + variables: { + input: { email, password }, + }, + }), }); - return { - success: true, - redirectTo: '/', - }; - } - return { - success: false, - error: { - name: 'LoginError', - message: 'Invalid username or password', - }, - }; - }, - register: async (params) => { - // Suppose we actually send a request to the back end here. - const user = mockUsers.find((item) => item.email === params.email); + const { data, errors } = await response.json(); + + if (errors) { + return { + success: false, + error: { + name: 'LoginError', + message: errors[0].message, + }, + }; + } + + if (data.loginAccount?.token) { + if (data.loginAccount.role !== 'ADMIN' && data.loginAccount.role !== 'SUPERADMIN') { + return { + success: false, + error: { + name: 'LoginError', + message: 'You are not authorized to login', + }, + }; + } + + Cookies.set( + 'auth', + JSON.stringify({ + token: data.loginAccount.token, + _id: data.loginAccount._id, + role: data.loginAccount.role, + }), + { + expires: 30, // 30 jours + path: '/', + }, + ); + + return { + success: true, + redirectTo: '/', + }; + } - if (user) { - Cookies.set('auth', JSON.stringify(user), { - expires: 30, // 30 days - path: '/', - }); return { - success: true, - redirectTo: '/', + success: false, + error: { + name: 'LoginError', + message: 'Invalid username or password', + }, + }; + } catch (error) { + return { + success: false, + error: { + name: 'LoginError', + message: (error as Error).message, + }, }; } - return { - success: false, - error: { - message: 'Register failed', - name: 'Invalid email or password', - }, - }; }, - forgotPassword: async (params) => { + forgotPassword: async () => { // Suppose we actually send a request to the back end here. - const user = mockUsers.find((item) => item.email === params.email); + const user = null; if (user) { //we can send email with reset password link here @@ -112,23 +123,71 @@ export const gqlAuthProvider: AuthProvider = { }, }; }, - updatePassword: async (params) => { - // Suppose we actually send a request to the back end here. - const isPasswordInvalid = params.password === '123456' || !params.password; + updatePassword: async ({ password }) => { + try { + const apiUrl = process.env.NEXT_PUBLIC_GQL_API_BASE_PATH; + if (!apiUrl) { + throw new Error('NEXT_PUBLIC_GQL_API_BASE_PATH is not defined'); + } - if (isPasswordInvalid) { + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${JSON.parse(Cookies.get('auth') || '{}')?.token}`, + }, + body: JSON.stringify({ + operationName: 'UpdateAccountPassword', + query: ` + mutation UpdateAccountPassword($input: AccountUpdatePasswordInput!) { + updateAccountPassword(input: $input) { + _id + } + } + `, + variables: { + input: { + _id: JSON.parse(Cookies.get('auth') || '{}')?._id, + password: password, + }, + }, + }), + }); + + const { data, errors } = await response.json(); + + if (errors) { + return { + success: false, + error: { + name: 'UpdateAccountPassword', + message: errors[0].message, + }, + }; + } + + if (data.updateAccountPassword) { + return { + success: true, + }; + } + + return { + success: false, + error: { + name: 'UpdateAccountPassword', + message: 'Invalid password', + }, + }; + } catch (error) { return { success: false, error: { - message: 'Update password failed', - name: 'Invalid password', + name: 'UpdateAccountPassword', + message: (error as Error).message, }, }; } - - return { - success: true, - }; }, logout: async () => { Cookies.remove('auth', { path: '/' }); @@ -155,7 +214,7 @@ export const gqlAuthProvider: AuthProvider = { const auth = Cookies.get('auth'); if (auth) { const parsedUser = JSON.parse(auth); - return parsedUser.roles; + return parsedUser.role; } return null; }, diff --git a/src/v6y-front-bo/src/infrastructure/types/FormType.ts b/src/v6y-front-bo/src/infrastructure/types/FormType.ts index e5dae7bd..7e3606e3 100644 --- a/src/v6y-front-bo/src/infrastructure/types/FormType.ts +++ b/src/v6y-front-bo/src/infrastructure/types/FormType.ts @@ -14,6 +14,7 @@ export interface FormQueryOptionsType { export interface FormMutationOptionsType { resource?: string; + editResource?: string; editQuery: string; editQueryParams?: Record; editFormAdapter?: (data: Record) => Variables; @@ -33,6 +34,7 @@ export interface FormWrapperProps { title?: string | ReactNode; queryOptions?: FormQueryOptionsType; mutationOptions?: FormMutationOptionsType; + createOptions?: FormCreateOptionsType; formItems?: ReactNode[]; selectOptions?: { resource: string;