diff --git a/README.md b/README.md index 55c4fa5..d70d8b4 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,4 @@ npm install ioredis npm install -D daisyui@latest npm install tailwindcss @tailwindcss/postcss daisyui@latest npm install lucide-react +npm install next-crypto diff --git a/ServicesApi/src/auth/login/dtoValidator.ts b/ServicesApi/src/auth/login/dtoValidator.ts index d7bba84..1cf5397 100644 --- a/ServicesApi/src/auth/login/dtoValidator.ts +++ b/ServicesApi/src/auth/login/dtoValidator.ts @@ -1,4 +1,4 @@ -import { IsObject, IsOptional, IsString, IsBoolean } from 'class-validator'; +import { IsOptional, IsString, IsBoolean } from 'class-validator'; export class userLoginValidator { @IsString() diff --git a/ServicesApi/src/auth/login/login.service.ts b/ServicesApi/src/auth/login/login.service.ts index bcdf499..6179126 100644 --- a/ServicesApi/src/auth/login/login.service.ts +++ b/ServicesApi/src/auth/login/login.service.ts @@ -17,40 +17,149 @@ export class LoginService { const foundUser = await this.prisma.users.findFirstOrThrow({ where: { email: dto.accessKey }, }); - if (foundUser.password_token) { throw new Error('Password need to be set first'); } - const isPasswordValid = this.passHandlers.check_password( foundUser.uu_id, dto.password, foundUser.hash_password, ); - if (!isPasswordValid) { throw new Error('Invalid password'); } - const foundPerson = await this.prisma.people.findFirstOrThrow({ where: { id: foundUser.id }, }); - const redisData = AuthTokenSchema.parse({ - people: foundPerson, - users: foundUser, - credentials: { - person_id: foundPerson.id, - person_name: foundPerson.firstname, - }, - selectionList: [], - }); - const accessToken = await this.redis.setLoginToRedis( - redisData, + const alreadyExists = await this.redis.callExistingLoginToken( foundUser.uu_id, ); - return { - accessToken, - message: 'Login successful', - }; + if (alreadyExists) { + return { + token: alreadyExists, + message: 'User already logged in', + }; + } else { + let selectList: any[] = []; + if (foundUser.user_type === 'occupant') { + const livingSpaces = await this.prisma.build_living_space.findMany({ + where: { people: { id: foundPerson.id } }, + orderBy: { + build_parts: { + build: { + id: 'asc', + }, + }, + }, + select: { + uu_id: true, + occupant_types: { + select: { + uu_id: true, + occupant_code: true, + occupant_type: true, + function_retriever: true, + }, + }, + build_parts: { + select: { + uu_id: true, + part_code: true, + part_no: true, + part_level: true, + human_livable: true, + api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown: { + select: { + uu_id: true, + enum_class: true, + value: true, + }, + }, + build: { + select: { + uu_id: true, + build_name: true, + }, + }, + }, + }, + }, + }); + selectList = livingSpaces; + } else if (foundUser.user_type === 'employee') { + const employees = await this.prisma.employees.findMany({ + where: { people: { id: foundPerson.id } }, + orderBy: { + staff: { + duties: { + departments: { + companies: { + formal_name: 'asc', + }, + }, + }, + }, + }, + select: { + uu_id: true, + staff: { + select: { + uu_id: true, + staff_code: true, + function_retriever: true, + duties: { + select: { + uu_id: true, + departments: { + select: { + uu_id: true, + department_code: true, + department_name: true, + companies: { + select: { + uu_id: true, + formal_name: true, + public_name: true, + addresses: { + select: { + comment_address: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + selectList = employees; + } + + const redisData = AuthTokenSchema.parse({ + people: foundPerson, + users: foundUser, + credentials: { + person_uu_id: foundPerson.uu_id, + person_name: foundPerson.firstname, + person_full_name: `${foundPerson.firstname} ${foundPerson.middle_name || ''} | ${foundPerson.birthname || ''} | ${foundPerson.surname}`, + }, + selectionList: { + type: foundUser.user_type, + list: selectList, + }, + }); + + const accessToken = await this.redis.setLoginToRedis( + redisData, + foundUser.uu_id, + ); + return { + token: accessToken, + message: 'Login successful', + }; + } } } diff --git a/ServicesApi/src/auth/select/dtoValidator.ts b/ServicesApi/src/auth/select/dtoValidator.ts index 6f9a14f..d94c00e 100644 --- a/ServicesApi/src/auth/select/dtoValidator.ts +++ b/ServicesApi/src/auth/select/dtoValidator.ts @@ -2,5 +2,5 @@ import { IsString } from 'class-validator'; export class userSelectValidator { @IsString() - selected_uu_id: string; + uuid: string; } diff --git a/ServicesApi/src/auth/select/select.service.ts b/ServicesApi/src/auth/select/select.service.ts index 6187ab2..84c7079 100644 --- a/ServicesApi/src/auth/select/select.service.ts +++ b/ServicesApi/src/auth/select/select.service.ts @@ -28,27 +28,53 @@ export class SelectService { ); } const accessToken = accessObject.key.split(':')[1]; - console.log('accessToken', accessToken); - + const existingSelectToken = await this.redis.callExistingSelectToken( + accessObject.value.users.uu_id, + dto.uuid, + ); + if (existingSelectToken) { + return { + message: 'Select successful', + token: existingSelectToken, + }; + } const userType = accessObject.value.users.user_type; if (userType === 'employee') { const employee = await this.prisma.employees.findFirstOrThrow({ - where: { uu_id: dto.selected_uu_id }, + where: { uu_id: dto.uuid }, + omit: { + id: true, + }, }); const staff = await this.prisma.staff.findFirstOrThrow({ where: { id: employee.staff_id }, + omit: { + id: true, + }, }); const duties = await this.prisma.duties.findFirstOrThrow({ where: { id: staff.duties_id }, + omit: { + id: true, + }, }); const department = await this.prisma.departments.findFirstOrThrow({ where: { id: duties.department_id }, + omit: { + id: true, + }, }); const duty = await this.prisma.duty.findFirstOrThrow({ where: { id: duties.duties_id }, + omit: { + id: true, + }, }); const company = await this.prisma.companies.findFirstOrThrow({ where: { id: duties.company_id }, + omit: { + id: true, + }, }); const employeeToken = EmployeeTokenSchema.parse({ @@ -59,9 +85,43 @@ export class SelectService { staff: staff, menu: null, pages: null, - config: null, - caches: null, - selection: null, + selection: await this.prisma.employees.findFirstOrThrow({ + where: { uu_id: dto.uuid }, + select: { + uu_id: true, + staff: { + select: { + uu_id: true, + staff_code: true, + function_retriever: true, + duties: { + select: { + uu_id: true, + departments: { + select: { + uu_id: true, + department_code: true, + department_name: true, + companies: { + select: { + uu_id: true, + formal_name: true, + public_name: true, + addresses: { + select: { + comment_address: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }), functionsRetriever: staff.function_retriever, kind: UserType.employee, }); @@ -70,7 +130,7 @@ export class SelectService { accessToken, employeeToken, accessObject.value.users.uu_id, - dto.selected_uu_id, + dto.uuid, ); return { @@ -79,19 +139,54 @@ export class SelectService { }; } else if (userType === 'occupant') { const livingSpace = await this.prisma.build_living_space.findFirstOrThrow( - { where: { uu_id: dto.selected_uu_id } }, + { + where: { uu_id: dto.uuid }, + omit: { + id: true, + person_id: true, + build_parts_id: true, + occupant_type_id: true, + ref_id: true, + replication_id: true, + cryp_uu_id: true, + }, + }, ); const occupantType = await this.prisma.occupant_types.findFirstOrThrow({ - where: { id: livingSpace.occupant_type_id }, + where: { uu_id: livingSpace.occupant_type_uu_id }, + omit: { + id: true, + cryp_uu_id: true, + ref_id: true, + replication_id: true, + }, }); const part = await this.prisma.build_parts.findFirstOrThrow({ - where: { id: livingSpace.build_parts_id }, + where: { uu_id: livingSpace.build_parts_uu_id }, + omit: { + id: true, + cryp_uu_id: true, + ref_id: true, + replication_id: true, + }, }); const build = await this.prisma.build.findFirstOrThrow({ - where: { id: part.build_id }, + where: { uu_id: part.build_uu_id }, + omit: { + id: true, + cryp_uu_id: true, + ref_id: true, + replication_id: true, + }, }); const company = await this.prisma.companies.findFirstOrThrow({ where: { uu_id: accessObject.value.users.related_company }, + omit: { + id: true, + cryp_uu_id: true, + ref_id: true, + replication_id: true, + }, }); const occupantToken = OccupantTokenSchema.parse({ livingSpace: livingSpace, @@ -103,7 +198,42 @@ export class SelectService { pages: null, config: null, caches: null, - selection: null, + selection: await this.prisma.build_living_space.findFirstOrThrow({ + where: { uu_id: dto.uuid }, + select: { + uu_id: true, + occupant_types: { + select: { + uu_id: true, + occupant_code: true, + occupant_type: true, + function_retriever: true, + }, + }, + build_parts: { + select: { + uu_id: true, + part_code: true, + part_no: true, + part_level: true, + human_livable: true, + api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown: { + select: { + uu_id: true, + enum_class: true, + value: true, + }, + }, + build: { + select: { + uu_id: true, + build_name: true, + }, + }, + }, + }, + }, + }), functionsRetriever: occupantType.function_retriever, kind: UserType.occupant, }); @@ -111,7 +241,7 @@ export class SelectService { accessToken, occupantToken, accessObject.value.users.uu_id, - dto.selected_uu_id, + dto.uuid, ); return { message: 'Select successful', diff --git a/ServicesApi/src/cache.service.ts b/ServicesApi/src/cache.service.ts index 839dbb1..cdf1f18 100644 --- a/ServicesApi/src/cache.service.ts +++ b/ServicesApi/src/cache.service.ts @@ -19,16 +19,7 @@ export class CacheService { if (!value) { return null; } - return JSON.parse(value); - } - - async get_with_keys(listKeys: (string | null)[]): Promise { - const joinKeys = this.createRegexPattern(listKeys); - const value = await this.client.get(joinKeys); - if (!value) { - return null; - } - return JSON.parse(value); + return { key, value: JSON.parse(value) }; } async set_with_ttl(key: string, value: any, ttl: number) { diff --git a/ServicesApi/src/main.ts b/ServicesApi/src/main.ts index a5673ef..466fe8b 100644 --- a/ServicesApi/src/main.ts +++ b/ServicesApi/src/main.ts @@ -5,8 +5,7 @@ import { PrismaService } from './prisma.service'; async function bootstrap() { const app = await NestFactory.create(AppModule); - await app.listen(process.env.PORT ?? 3000); - + await app.listen(process.env.PORT ?? 8001); console.log(`🚀 Uygulama çalışıyor: ${await app.getUrl()}`); extractAndPersistRoutes(app, app.get(PrismaService)); } diff --git a/ServicesApi/src/types/auth/token.ts b/ServicesApi/src/types/auth/token.ts index e97ccb4..ead2205 100644 --- a/ServicesApi/src/types/auth/token.ts +++ b/ServicesApi/src/types/auth/token.ts @@ -9,8 +9,9 @@ export type UserType = (typeof UserType)[keyof typeof UserType]; // Credentials export const CredentialsSchema = z.object({ - person_id: z.number(), + person_uu_id: z.string(), person_name: z.string(), + full_name: z.string(), }); export type Credentials = z.infer; @@ -42,7 +43,7 @@ export const AuthTokenSchema = z.object({ active: z.boolean(), is_notification_send: z.boolean(), is_email_send: z.boolean(), - id: z.number(), + // id: z.number(), uu_id: z.string(), expiry_starts: z.date(), expiry_ends: z.date(), @@ -77,7 +78,7 @@ export const AuthTokenSchema = z.object({ active: z.boolean(), is_notification_send: z.boolean(), is_email_send: z.boolean(), - id: z.number(), + // id: z.number(), uu_id: z.string(), expiry_starts: z.date(), expiry_ends: z.date(), @@ -86,14 +87,23 @@ export const AuthTokenSchema = z.object({ default_language: z.string(), }), credentials: CredentialsSchema, - selectionList: z.array(z.any()).optional().default([]), + selectionList: z + .object({ + type: z.string(), + list: z.array(z.any()).optional().default([]), + }) + .optional() + .default({ + type: '', + list: [], + }), }); export type AuthToken = z.infer; export const EmployeeTokenSchema = z.object({ company: z.object({ - id: z.number(), + // id: z.number(), uu_id: z.string(), formal_name: z.string(), company_type: z.string(), @@ -107,13 +117,13 @@ export const EmployeeTokenSchema = z.object({ is_blacklist: z.boolean(), parent_id: z.number().nullable(), workplace_no: z.string().nullable(), - official_address_id: z.number().nullable(), + // official_address_id: z.number().nullable(), official_address_uu_id: z.string().nullable(), top_responsible_company_id: z.number().nullable(), top_responsible_company_uu_id: z.string().nullable(), ref_id: z.string().nullable(), - replication_id: z.number(), - cryp_uu_id: z.string().nullable(), + // replication_id: z.number(), + // cryp_uu_id: z.string().nullable(), created_credentials_token: z.string().nullable(), updated_credentials_token: z.string().nullable(), confirmed_credentials_token: z.string().nullable(), @@ -129,17 +139,17 @@ export const EmployeeTokenSchema = z.object({ ref_int: z.number().nullable(), }), department: z.object({ - id: z.number(), + // id: z.number(), uu_id: z.string(), parent_department_id: z.number().nullable(), department_code: z.string(), department_name: z.string(), department_description: z.string(), - company_id: z.number(), + // company_id: z.number(), company_uu_id: z.string(), - ref_id: z.string().nullable(), - replication_id: z.number(), - cryp_uu_id: z.string().nullable(), + // ref_id: z.string().nullable(), + // replication_id: z.number(), + // cryp_uu_id: z.string().nullable(), created_credentials_token: z.string().nullable(), updated_credentials_token: z.string().nullable(), confirmed_credentials_token: z.string().nullable(), @@ -155,14 +165,14 @@ export const EmployeeTokenSchema = z.object({ ref_int: z.number().nullable(), }), duty: z.object({ - id: z.number(), + // id: z.number(), uu_id: z.string(), duty_name: z.string(), duty_code: z.string(), duty_description: z.string(), - ref_id: z.string().nullable(), - replication_id: z.number(), - cryp_uu_id: z.string().nullable(), + // ref_id: z.string().nullable(), + // replication_id: z.number(), + // cryp_uu_id: z.string().nullable(), created_credentials_token: z.string().nullable(), updated_credentials_token: z.string().nullable(), confirmed_credentials_token: z.string().nullable(), @@ -178,15 +188,15 @@ export const EmployeeTokenSchema = z.object({ ref_int: z.number().nullable(), }), employee: z.object({ - id: z.number(), + // id: z.number(), uu_id: z.string(), staff_id: z.number(), staff_uu_id: z.string(), people_id: z.number(), people_uu_id: z.string(), - ref_id: z.string().nullable(), - replication_id: z.number(), - cryp_uu_id: z.string().nullable(), + // ref_id: z.string().nullable(), + // replication_id: z.number(), + // cryp_uu_id: z.string().nullable(), created_credentials_token: z.string().nullable(), updated_credentials_token: z.string().nullable(), confirmed_credentials_token: z.string().nullable(), @@ -202,17 +212,17 @@ export const EmployeeTokenSchema = z.object({ ref_int: z.number().nullable(), }), staff: z.object({ - id: z.number(), + // id: z.number(), uu_id: z.string(), staff_description: z.string(), staff_name: z.string(), staff_code: z.string(), - duties_id: z.number(), + // duties_id: z.number(), duties_uu_id: z.string(), function_retriever: z.string().nullable(), - ref_id: z.string().nullable(), - replication_id: z.number(), - cryp_uu_id: z.string().nullable(), + // ref_id: z.string().nullable(), + // replication_id: z.number(), + // cryp_uu_id: z.string().nullable(), created_credentials_token: z.string().nullable(), updated_credentials_token: z.string().nullable(), confirmed_credentials_token: z.string().nullable(), @@ -230,8 +240,6 @@ export const EmployeeTokenSchema = z.object({ menu: z.array(z.object({})).nullable(), pages: z.array(z.string()).nullable(), - // config: z.record(z.string(), z.unknown()).nullable(), - // caches: z.record(z.string(), z.unknown()).nullable(), selection: z.record(z.string(), z.unknown()).nullable(), functionsRetriever: z.string(), @@ -247,8 +255,6 @@ export const OccupantTokenSchema = z.object({ menu: z.array(z.object({})).nullable(), pages: z.array(z.string()).nullable(), - // config: z.record(z.string(), z.unknown()).nullable(), - // caches: z.record(z.string(), z.unknown()).nullable(), selection: z.record(z.string(), z.unknown()).nullable(), functionsRetriever: z.string(), diff --git a/ServicesApi/src/utils/auth/redis_handlers.ts b/ServicesApi/src/utils/auth/redis_handlers.ts index 855249c..c125bea 100644 --- a/ServicesApi/src/utils/auth/redis_handlers.ts +++ b/ServicesApi/src/utils/auth/redis_handlers.ts @@ -71,6 +71,18 @@ export class RedisHandlers { return mergedRedisKey; } + public mergeLoginUser(userUUID: string) { + const mergedRedisKey = `${this.AUTH_TOKEN}:*:*:${userUUID}:${userUUID}`; + this.validateRedisKey(mergedRedisKey, this.AUTH_TOKEN); + return mergedRedisKey; + } + + public mergeSelectUser(userUUID: string, livingUUID: string) { + const mergedRedisKey = `${this.SELECT_TOKEN}:*:*:${userUUID}:${livingUUID}`; + this.validateRedisKey(mergedRedisKey, this.SELECT_TOKEN); + return mergedRedisKey; + } + generateSelectToken(accessToken: string, userUUID: string) { return this.passwordService.createSelectToken(accessToken, userUUID); } @@ -154,6 +166,53 @@ export class RedisHandlers { : null; } + async callExistingLoginToken(userUUID: string): Promise { + const mergedKey = this.mergeLoginUser(userUUID); + if (!mergedKey.includes('*')) { + throw new ForbiddenException( + 'Authorization failed - No valid select keys', + ); + } + const keys = await this.scanKeys(mergedKey, this.AUTH_TOKEN); + if (keys.length === 0) { + return null; + } + for (const key of keys) { + const value = await this.cacheService.get(key); + if (value) { + this.cacheService.set_with_ttl(value.key, value.value, 60 * 30); + const token = value.key.split(':')[1]; + return token; + } + } + throw new ForbiddenException('Authorization failed - No valid login keys'); + } + + async callExistingSelectToken( + userUUID: string, + uuid: string, + ): Promise { + const mergedKey = this.mergeSelectUser(userUUID, uuid); + if (!mergedKey.includes('*')) { + throw new ForbiddenException( + 'Authorization failed - No valid select keys', + ); + } + const keys = await this.scanKeys(mergedKey, this.SELECT_TOKEN); + if (keys.length === 0) { + return null; + } + for (const key of keys) { + const value = await this.cacheService.get(key); + if (value) { + this.cacheService.set_with_ttl(value.key, value.value, 60 * 30); + const token = value.key.split(':')[2]; + return token; + } + } + throw new ForbiddenException('Authorization failed - No valid select keys'); + } + async deleteLoginFromRedis(req: Request): Promise { const mergedKey = this.mergeLoginKey(req); return this.cacheService.delete(mergedKey); diff --git a/ServicesFrontEnd/frontend/messages/en.json b/ServicesFrontEnd/frontend/messages/en.json index b3cd6ff..a42d9ec 100644 --- a/ServicesFrontEnd/frontend/messages/en.json +++ b/ServicesFrontEnd/frontend/messages/en.json @@ -16,5 +16,31 @@ }, "LocaleLayout": { "title": "Next.js i18n Application" + }, + "Select": { + "title": "Select Your Option", + "description": "Please select one of the following options to continue", + "employee": "Employee", + "staff": "Staff", + "uuid": "UUID", + "department": "Department", + "name": "Name", + "code": "Code", + "company": "Company", + "occupant": "Occupant", + "occupant_code": "Occupant Code", + "building": "Building", + "type": "Type", + "part_details": "Part Details", + "no": "No", + "level": "Level", + "status": "Status", + "livable": "Livable", + "not_livable": "Not Livable", + "selection": "Selection", + "id": "ID", + "processing": "Processing...", + "continue": "Continue", + "select_option": "Select an option to continue" } } diff --git a/ServicesFrontEnd/frontend/messages/tr.json b/ServicesFrontEnd/frontend/messages/tr.json index d1b71ca..49a7a47 100644 --- a/ServicesFrontEnd/frontend/messages/tr.json +++ b/ServicesFrontEnd/frontend/messages/tr.json @@ -16,5 +16,31 @@ }, "LocaleLayout": { "title": "Next.js i18n Uygulaması" + }, + "Select": { + "title": "Seçeneğinizi Seçin", + "description": "Devam etmek için lütfen aşağıdaki seçeneklerden birini seçin", + "employee": "Çalışan", + "staff": "Personel", + "uuid": "UUID", + "department": "Departman", + "name": "İsim", + "code": "Kod", + "company": "Şirket", + "occupant": "Oturak", + "occupant_code": "Oturak Kodu", + "building": "Bina", + "type": "Tip", + "part_details": "Parça Detayları", + "no": "No", + "level": "Seviye", + "status": "Durum", + "livable": "Yaşanabilir", + "not_livable": "Yaşanamaz", + "selection": "Seçim", + "id": "ID", + "processing": "İşleniyor...", + "continue": "Devam Et", + "select_option": "Devam etmek için bir seçenek seçin" } } diff --git a/ServicesFrontEnd/frontend/package-lock.json b/ServicesFrontEnd/frontend/package-lock.json index 8accf7c..e92aafd 100644 --- a/ServicesFrontEnd/frontend/package-lock.json +++ b/ServicesFrontEnd/frontend/package-lock.json @@ -9,9 +9,11 @@ "version": "0.1.0", "dependencies": { "clsx": "^2.1.1", + "cookies-next": "^6.1.0", "ioredis": "^5.6.1", "lucide-react": "^0.533.0", "next": "15.4.4", + "next-crypto": "^1.0.8", "next-intl": "^4.3.4", "react": "19.1.0", "react-dom": "19.1.0", @@ -1154,6 +1156,28 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cookies-next": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-6.1.0.tgz", + "integrity": "sha512-8MqWliHg6YRatqlup5HlKCqXM5cFtwq9BVowDpPniPfbTOmrfIEXUQOcRFVXQltV+hyvKDRGJPNtceICkiJ/IA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1" + }, + "peerDependencies": { + "next": ">=15.0.0", + "react": ">= 16.8.0" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -1681,6 +1705,12 @@ } } }, + "node_modules/next-crypto": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/next-crypto/-/next-crypto-1.0.8.tgz", + "integrity": "sha512-6VcrH+xFuuCRGCdDMjFFibhJ97c4s+J/6SEV73RUYJhh38MDW4WXNZNTWIMZBq0B29LOIfAQ0XA37xGUZZCCjA==", + "license": "MIT" + }, "node_modules/next-intl": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.4.tgz", diff --git a/ServicesFrontEnd/frontend/package.json b/ServicesFrontEnd/frontend/package.json index ed5bda8..197c29f 100644 --- a/ServicesFrontEnd/frontend/package.json +++ b/ServicesFrontEnd/frontend/package.json @@ -10,9 +10,11 @@ }, "dependencies": { "clsx": "^2.1.1", + "cookies-next": "^6.1.0", "ioredis": "^5.6.1", "lucide-react": "^0.533.0", "next": "15.4.4", + "next-crypto": "^1.0.8", "next-intl": "^4.3.4", "react": "19.1.0", "react-dom": "19.1.0", diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/LoginPage.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/LoginPage.tsx index 77b9a9a..b784c30 100644 --- a/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/LoginPage.tsx +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/LoginPage.tsx @@ -3,109 +3,60 @@ import Link from 'next/link'; import { useState } from 'react'; import { useTranslations } from 'next-intl'; import { z } from 'zod'; -import { Eye, EyeOff, Lock, Mail, User } from "lucide-react"; - -const loginSchema = z.object({ - email: z.string().email('Invalid email'), - password: z.string().min(6, 'Password too short') -}); - -type LoginFormData = z.infer; - -const getRandomColor = () => { - const colors = ['bg-indigo-500', 'bg-purple-500', 'bg-pink-500', 'bg-blue-500', 'bg-teal-500']; - return colors[Math.floor(Math.random() * colors.length)]; -}; +import { Eye, EyeOff, Lock, Mail } from "lucide-react"; +import { apiPostFetcher } from '@/lib/fetcher'; +import { useRouter } from '@/i18n/routing'; export default function LoginPage() { const t = useTranslations('Login'); - const [formData, setFormData] = useState({ - email: '', - password: '' + const loginSchema = z.object({ + accessKey: z.string().email(t('emailWrong')), + password: z.string().min(6, t('passwordWrong')), + rememberMe: z.boolean().default(false), }); - const [errors, setErrors] = useState>({}); + type LoginInterface = z.infer; + interface LoginFormErrors { + accessKey?: boolean; + password?: boolean; + rememberMe?: boolean; + } + const [errors, setErrors] = useState({}); const [showPassword, setShowPassword] = useState(false); - const [isAccordionOpen, setIsAccordionOpen] = useState(false); - const recentUser = { - name: 'Mika Lee', - initial: 'M', - color: getRandomColor() - }; - - const handleChange = (e: React.ChangeEvent) => { - setFormData({ ...formData, [e.target.name]: e.target.value }); - }; - - const handleSubmit = async (e: React.FormEvent) => { + const router = useRouter(); + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - try { - loginSchema.parse(formData); - setErrors({}); - console.log('Form submitted:', formData); - // Here you would typically call your authentication API - } catch (err) { - if (err instanceof z.ZodError) { - const fieldErrors: Partial = {}; - Object.entries(err.flatten().fieldErrors).forEach(([key, value]) => { - if (Array.isArray(value) && value.length > 0) { - const fieldKey = key as keyof LoginFormData; - fieldErrors[fieldKey] = value[0]; - } - }); - setErrors(fieldErrors); + const form = e.currentTarget; + const formData = new FormData(form); + const loginData: LoginInterface = { + accessKey: formData.get('email') as string, + password: formData.get('password') as string, + rememberMe: formData.get('rememberMe') === 'on' + }; + const result = loginSchema.safeParse(loginData); + if (!result.success) { + const fieldErrors: LoginFormErrors = {}; + if (result.error.issues.some(issue => issue.path.includes('email'))) { + fieldErrors.accessKey = true; } + if (result.error.issues.some(issue => issue.path.includes('password'))) { + fieldErrors.password = true; + } + setErrors(fieldErrors); + } else { + setErrors({}) + console.log('Form submitted successfully:', loginData); + apiPostFetcher({ url: '/api/auth/login', body: loginData, isNoCache: true }).then((res) => { + if (res.success) { + console.log('Login successful, redirecting to select page'); + router.push('/select'); + } + }).catch((error) => { console.error('Login failed:', error) }); } }; return (
- {/* Recent logins accordion */} - {/*
-
-
setIsAccordionOpen(!isAccordionOpen)} - > -
-
-
- -
-
-
-

{t('recentLogins')}

-
-
- - - -
- - {isAccordionOpen && ( -
-

{t('clickPictureOrAdd')}

-
-
-
- + -
- - {t('addAccount')} - -
-
-
- )} -
-
*/} -
{/* Left side - Login form (now takes full width) */}
@@ -122,27 +73,25 @@ export default function LoginPage() {
- {errors.email && ( + {errors.accessKey && (
- {errors.email} + {t('emailWrong')}
)} @@ -156,14 +105,12 @@ export default function LoginPage() {
)} @@ -189,7 +136,7 @@ export default function LoginPage() {
diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/page.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/page.tsx index a13a157..0eefed0 100644 --- a/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/page.tsx +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/page.tsx @@ -1,21 +1,18 @@ 'use server'; import LocaleSwitcherServer from '@/components/LocaleSwitcherServer'; import LoginPage from './LoginPage'; -import FromFigma from './fromFigma'; +import { Locale } from 'next-intl'; +import { checkAccessOnLoginPage } from '@/app/api/guards'; -type Props = { - params: Promise<{ locale: string }>; -}; - -export default async function PageLogin({ params }: Props) { +export default async function PageLogin({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; + await checkAccessOnLoginPage(locale as Locale); return (
- {/* */}
); } \ No newline at end of file diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/select/SelectPage.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/select/SelectPage.tsx new file mode 100644 index 0000000..86d06ab --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/select/SelectPage.tsx @@ -0,0 +1,282 @@ +'use client'; +import { useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; +import { useRouter } from 'next/navigation'; +import { apiGetFetcher } from '@/lib/fetcher'; + +export default function PageSelect() { + const t = useTranslations('Select'); + const router = useRouter(); + const [selectionList, setSelectionList] = useState<{ type: string, list: any[] } | null>(null); + const [selectedOption, setSelectedOption] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const fetchSelectionList = async () => { + setIsLoading(true); + try { + apiGetFetcher({ url: '/api/auth/selections', isNoCache: true }).then((res) => { + if (res.success) { + if (res.data && typeof res.data === 'object' && 'type' in res.data && 'list' in res.data) { + setSelectionList(res.data as { type: string, list: any[] }); + } + } + }) + } catch (error) { + console.error('Error fetching selection list:', error); + } finally { + setIsLoading(false); + } + }; + fetchSelectionList(); + }, []); + const handleSelection = (id: string) => { setSelectedOption(id) }; + const handleContinue = async () => { + if (!selectedOption) return; + setIsLoading(true); + try { + console.log('Selected option:', selectedOption); + const payload = { uuid: selectedOption }; + const response = await fetch('/api/auth/select', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); + const result = await response.json(); + if (response.ok && result.status === 200) { + console.log('Selection successful, redirecting to venue page'); + router.push('/venue'); + } else { + console.error('Selection failed:', result.message); + alert(`Selection failed: ${result.message || 'Unknown error'}`); + } + } catch (error) { + console.error('Error submitting selection:', error); + alert('An error occurred while submitting your selection. Please try again.'); + } finally { setIsLoading(false) } + }; + + return ( +
+
+
+

{t('title')}

+

{t('description')}

+
+ +
+
+ {selectionList?.list?.map((item: any) => { + if (selectionList.type === 'employee') { + const staff = item.staff; + const department = staff?.duties?.departments; + const company = department?.companies; + + return ( +
handleSelection(item.uu_id)} + > +
+
+
+ {staff?.staff_code?.charAt(0) || 'E'} +
+

{t('staff')}: {staff?.staff_code || t('employee')}

+
+
+
+ + + + {t('uuid')}: + {item?.uu_id} +
+ +
+
+ + + + {t('department')} +
+ +
+
+ {t('name')}: + {department?.department_name || 'N/A'} +
+
+ {t('code')}: + {department?.department_code || 'N/A'} +
+
+
+ +
+ + + + {t('company')}: + {company?.public_name || company?.formal_name || 'N/A'} +
+
+
+ + {selectedOption === item.uu_id && ( +
+
+ + + +
+
+ )} +
+ ); + } + + if (selectionList.type === 'occupant') { + const occupantType = item.occupant_types; + const buildPart = item.build_parts; + const build = buildPart?.build; + const enums = buildPart?.api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown; + return ( +
handleSelection(item.uu_id)} + > +
+
+
+ {occupantType?.occupant_code?.charAt(0) || 'O'} +
+

{t('occupant_type')}: {occupantType?.occupant_type}

+
+
+
+ + + + {t('uuid')}: + {item?.uu_id} +
+
+ + + + {t('occupant_code')}: + {occupantType?.occupant_code} +
+ +
+ + + + {t('building')}: + {build?.build_name || 'Building'} +
+ +
+ + + + {t('type')}: + {enums?.value} +
+ +
+
+ + + + {t('part_details')} +
+ +
+
+ {t('code')}: + {buildPart?.part_code} +
+
+ {t('no')}: + {buildPart?.part_no} +
+
+ {t('level')}: + {buildPart?.part_level} +
+
+ {t('status')}: + + {buildPart?.human_livable ? t('livable') : t('not_livable')} + +
+
+
+
+
+ + {selectedOption === item.uu_id && ( +
+
+ + + +
+
+ )} +
+ ); + } + + return ( +
handleSelection(item.uu_id)} + > +
+
+
+ {item.uu_id?.charAt(0) || 'S'} +
+

{selectionList.type || t('selection')}

+
+

{item.uu_id || t('id')}

+
+ + {selectedOption === item.uu_id && ( +
+
+ + + +
+
+ )} +
+ ); + })} +
+ +
+ +
+
+
+
+ ); +} diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/select/page.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/select/page.tsx index eb38269..81f490e 100644 --- a/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/select/page.tsx +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/select/page.tsx @@ -1,3 +1,11 @@ -export default function SelectPage() { - return
; +'use server'; +import { Locale } from 'next-intl'; +import { checkAccess, checkSelectionOnSelectPage } from '@/app/api/guards'; +import SelectPageClient from './SelectPage'; + +export default async function PageSelect({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + await checkAccess(locale as Locale); + await checkSelectionOnSelectPage(locale as Locale); + return ; } diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/layout.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/layout.tsx index fedfbbc..34d36e4 100644 --- a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/layout.tsx +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/layout.tsx @@ -20,13 +20,18 @@ function removeSubStringFromPath(headersList: Headers) { return removeLocaleFromPath(currentRoute); } +function getLocaleFromPath(path: string) { + const locale = path.split('/')[0]; + return locale; +} + export default async function ProtectedLayout({ children, }: { children: ReactNode, }) { - // Server component approach to get URL const headersList = await headers(); + // const locale = getLocaleFromPath(removeSubStringFromPath(headersList)); const removedLocaleRoute = removeSubStringFromPath(headersList); console.log('Removed locale route:', removedLocaleRoute); return <>{children}; diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/office/page.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/office/page.tsx index 1944a65..ea390ba 100644 --- a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/office/page.tsx +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/office/page.tsx @@ -1,3 +1,3 @@ export default function OfficePage() { - return
; + return
Office Page
; } diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/venue/page.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/venue/page.tsx index 8ca801a..fc570b7 100644 --- a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/venue/page.tsx +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/venue/page.tsx @@ -1,3 +1,3 @@ export default function VenuePage() { - return
; + return
Venue Page
; } diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/venue/trial/page.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/venue/trial/page.tsx index 937bba1..81b59f0 100644 --- a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/venue/trial/page.tsx +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/venue/trial/page.tsx @@ -35,7 +35,7 @@ type SearchFormData = z.infer; export default function TrialPage() { const pathname = usePathname(); - const cleanPathname = removeLocaleFromPath(pathname); + const cleanPathname = removeLocaleFromPath(pathname || ''); const cacheKeyCreateForm = buildCacheKey({ url: cleanPathname, form: 'trialCreateForm', field: 'trialCreateField' }); const cacheKeySelectForm = buildCacheKey({ url: cleanPathname, form: 'trialSelectForm', field: 'trialSelectField' }); @@ -67,7 +67,7 @@ export default function TrialPage() { } catch (error) { console.error('Error saving form data:', error); setCreateFormErrors({ - error: "Error saving form data" + error: "Error saving form data" }); } } else { diff --git a/ServicesFrontEnd/frontend/src/app/api/auth/guard/access/route.ts b/ServicesFrontEnd/frontend/src/app/api/auth/guard/access/route.ts new file mode 100644 index 0000000..8389109 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/auth/guard/access/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; +import { isAccessTokenValid } from "@/fetchers/token/access"; + +export async function GET() { + const isValid = await isAccessTokenValid(); + return !isValid + ? NextResponse.json({ ok: false }, { status: 401 }) + : NextResponse.json({ ok: true }, { status: 200 }); +} diff --git a/ServicesFrontEnd/frontend/src/app/api/auth/guard/select/route.ts b/ServicesFrontEnd/frontend/src/app/api/auth/guard/select/route.ts new file mode 100644 index 0000000..45aa60d --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/auth/guard/select/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; +import { isSelectTokenValid } from "@/fetchers/token/select"; + +export async function GET() { + const isSlcTokenValid = await isSelectTokenValid(); + return !isSlcTokenValid + ? NextResponse.json({ ok: false }, { status: 401 }) + : NextResponse.json({ ok: true }, { status: 200 }); +} diff --git a/ServicesFrontEnd/frontend/src/app/api/auth/login/route.ts b/ServicesFrontEnd/frontend/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..4adb3af --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/auth/login/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; +import { doLogin } from "@/fetchers/auth/login/fetch"; +import { setCookieAccessToken } from "@/fetchers/token/cookies"; + +export async function POST(req: NextRequest) { + try { + const { accessKey, password, rememberMe } = await req.json(); + console.log("Login attempt for:", accessKey); + const response = await doLogin({ accessKey, password, rememberMe }); + if (response.status !== 200) { + console.log("Login failed with status:", response.status); + return NextResponse.json({ status: 401 }); + } + const data = response.data as any; + const token = data.token; + console.log("Token received:", token ? "[PRESENT]" : "[MISSING]"); + if (!token) { + console.error("No token received from login response"); + return NextResponse.json({ status: 500, message: "No token received" }); + } + await setCookieAccessToken(token); + console.log("Cookie set via setCookieAccessToken"); + return NextResponse.json({ status: 200 }); + } catch (error) { + console.error("Error in login route:", error); + return NextResponse.json({ status: 500, message: "Internal server error" }); + } +} diff --git a/ServicesFrontEnd/frontend/src/app/api/auth/select/route.ts b/ServicesFrontEnd/frontend/src/app/api/auth/select/route.ts new file mode 100644 index 0000000..d205915 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/auth/select/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from "next/server"; +import { doSelect } from "@/fetchers/auth/login/fetch"; +import { setCookieSelectToken } from "@/fetchers/token/cookies"; + +export async function POST(req: NextRequest) { + try { + const { uuid } = await req.json(); + console.log("Select attempt for UUID:", uuid); + + const response = await doSelect({ uuid }); + + if (response.status !== 200) { + console.log("Select failed with status:", response.status); + return NextResponse.json({ status: 401, message: "Select failed" }); + } + + const data = response.data as any; + const token = data.token; + console.log("Select token received:", token ? "[PRESENT]" : "[MISSING]"); + + if (!token) { + console.error("No token received from select response"); + return NextResponse.json({ status: 500, message: "No token received" }); + } + + // Set the cookie using the server-side utility function + await setCookieSelectToken(token); + console.log("Select cookie set via setCookieSelectToken"); + + // Return the response + return NextResponse.json({ status: 200 }); + } catch (error) { + console.error("Error in select route:", error); + return NextResponse.json({ status: 500, message: "Internal server error" }); + } +} diff --git a/ServicesFrontEnd/frontend/src/app/api/auth/selections/route.ts b/ServicesFrontEnd/frontend/src/app/api/auth/selections/route.ts new file mode 100644 index 0000000..754ba66 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/auth/selections/route.ts @@ -0,0 +1,6 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getSelectionList } from "@/fetchers/auth/selection/list/fetch"; + +export async function GET(req: NextRequest) { + return await getSelectionList(); +} \ No newline at end of file diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/delete/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/delete/route.ts index 72aaddc..929b0f8 100644 --- a/ServicesFrontEnd/frontend/src/app/api/cache/delete/route.ts +++ b/ServicesFrontEnd/frontend/src/app/api/cache/delete/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { deleteKey } from "@/libss/redisService"; +import { deleteKey } from "@/fetchers/redis/redisService"; export async function POST(req: NextRequest) { const { key } = await req.json(); diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/exists/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/exists/route.ts index 38b7ff4..712cdc1 100644 --- a/ServicesFrontEnd/frontend/src/app/api/cache/exists/route.ts +++ b/ServicesFrontEnd/frontend/src/app/api/cache/exists/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { exists } from "@/libss/redisService"; +import { exists } from "@/fetchers/redis/redisService"; export async function POST(req: NextRequest) { const { key } = await req.json(); diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/get/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/get/route.ts index 4d7dcad..8ddcb35 100644 --- a/ServicesFrontEnd/frontend/src/app/api/cache/get/route.ts +++ b/ServicesFrontEnd/frontend/src/app/api/cache/get/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { getJSON } from "@/libss/redisService"; +import { getJSON } from "@/fetchers/redis/redisService"; export async function POST(req: NextRequest) { const { key } = await req.json(); diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/renew/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/renew/route.ts index f61c16b..c8807b3 100644 --- a/ServicesFrontEnd/frontend/src/app/api/cache/renew/route.ts +++ b/ServicesFrontEnd/frontend/src/app/api/cache/renew/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { updateField } from "@/libss/redisService"; +import { updateField } from "@/fetchers/redis/redisService"; export async function POST(req: NextRequest) { try { diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/route.ts deleted file mode 100644 index a5390a4..0000000 --- a/ServicesFrontEnd/frontend/src/app/api/cache/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { - setJSON, - getJSON, - updateJSON, - deleteKey, - exists, -} from "@/libss/redisService"; - -export async function POST(req: NextRequest) { - try { - const { action, key, value, ttlSeconds } = await req.json(); - - switch (action) { - case "set": - await setJSON({ key, value, ttlSeconds }); - return NextResponse.json({ status: "ok", message: `Set ${key}` }); - - case "get": { - const result = await getJSON({ key }); - return result - ? NextResponse.json({ status: "ok", data: result }) - : NextResponse.json({ status: "not_found" }, { status: 404 }); - } - - case "update": - await updateJSON({ key, value }); - return NextResponse.json({ status: "ok", message: `Updated ${key}` }); - - case "delete": - await deleteKey({ key }); - return NextResponse.json({ status: "ok", message: `Deleted ${key}` }); - - case "exists": - const doesExist = await exists({ key }); - return NextResponse.json({ status: "ok", exists: doesExist }); - - default: - return NextResponse.json({ error: "Invalid action" }, { status: 400 }); - } - } catch (e: any) { - console.error("[redis-api] error:", e.message); - return NextResponse.json({ error: e.message }, { status: 500 }); - } -} diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/set/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/set/route.ts index eca69bf..d80f1bc 100644 --- a/ServicesFrontEnd/frontend/src/app/api/cache/set/route.ts +++ b/ServicesFrontEnd/frontend/src/app/api/cache/set/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { setJSON } from "@/libss/redisService"; +import { setJSON } from "@/fetchers/redis/redisService"; export async function POST(req: NextRequest) { const { key, value, ttlSeconds } = await req.json(); diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/update/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/update/route.ts index eb25214..04df7e8 100644 --- a/ServicesFrontEnd/frontend/src/app/api/cache/update/route.ts +++ b/ServicesFrontEnd/frontend/src/app/api/cache/update/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { updateJSON } from "@/libss/redisService"; +import { updateJSON } from "@/fetchers/redis/redisService"; export async function POST(req: NextRequest) { try { diff --git a/ServicesFrontEnd/frontend/src/app/api/guards.tsx b/ServicesFrontEnd/frontend/src/app/api/guards.tsx new file mode 100644 index 0000000..e9d1f74 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/guards.tsx @@ -0,0 +1,35 @@ +'use server'; +import { redirect } from '@/i18n/navigation'; +import { Locale } from 'next-intl'; +import { isAccessTokenValid } from '@/fetchers/token/access'; +import { isSelectTokenValid } from '@/fetchers/token/select'; + +async function checkAccessOnLoginPage(locale: Locale) { + const access = await isAccessTokenValid(); + if (access) { + return redirect({ href: '/select', locale: locale }); + } +} + +async function checkAccess(locale: Locale) { + const access = await isAccessTokenValid(); + if (!access) { + return redirect({ href: '/login', locale: locale }); + } +} + +async function checkSelectionOnSelectPage(locale: Locale) { + const select = await isSelectTokenValid(); + if (select) { + return redirect({ href: '/venue', locale: locale }); + } +} + +async function checkSelection(locale: Locale) { + const select = await isSelectTokenValid(); + if (!select) { + return redirect({ href: '/select', locale: locale }); + } 1 +} + +export { checkAccess, checkSelection, checkAccessOnLoginPage, checkSelectionOnSelectPage }; diff --git a/ServicesFrontEnd/frontend/src/app/home-page.tsx b/ServicesFrontEnd/frontend/src/app/home-page.tsx index 54c90ad..0615020 100644 --- a/ServicesFrontEnd/frontend/src/app/home-page.tsx +++ b/ServicesFrontEnd/frontend/src/app/home-page.tsx @@ -6,6 +6,8 @@ import LocaleSwitcherClient from '@/components/LocaleSwitcherClient'; export default function HomePage() { const t = useTranslations('Index'); + const n = useTranslations('Index.navigation'); + const router = useRouter(); const params = useParams(); @@ -18,11 +20,13 @@ export default function HomePage() {

{t('title')}

{t('description')}

-

{t('navigation.title')} : {params.locale}

+

{n('title')} : {params?.locale || 'tr'}

- - + + + +
); diff --git a/ServicesFrontEnd/frontend/src/fetchers/auth/login/fetch.tsx b/ServicesFrontEnd/frontend/src/fetchers/auth/login/fetch.tsx new file mode 100644 index 0000000..507119c --- /dev/null +++ b/ServicesFrontEnd/frontend/src/fetchers/auth/login/fetch.tsx @@ -0,0 +1,37 @@ +'use server'; +import { fetchData } from "@/fetchers/fecther"; +import { urlSelectEndpoint, urlLoginEndpoint } from "@/fetchers/urls"; +import { LoginViaAccessKeys } from "@/fetchers/types/login/validations"; +import { setCookieAccessToken, setCookieSelectToken } from "@/fetchers/token/cookies"; + + +async function doLogin(payload: LoginViaAccessKeys) { + const response = await fetchData( + urlLoginEndpoint, + payload, + "POST", + false, + 5000 + ); + if (response.status !== 200) { + console.log('doLogin response', response); + } + return response; +} + +async function doSelect(payload: { uuid: string }) { + const response = await fetchData( + urlSelectEndpoint, + payload, + "POST", + false, + 5000 + ); + if (response.status == 200) { + console.log('doSelect response', response); + + } + return response; +} + +export { doLogin, doSelect }; \ No newline at end of file diff --git a/ServicesFrontEnd/frontend/src/fetchers/auth/selection/list/fetch.tsx b/ServicesFrontEnd/frontend/src/fetchers/auth/selection/list/fetch.tsx new file mode 100644 index 0000000..90e59fd --- /dev/null +++ b/ServicesFrontEnd/frontend/src/fetchers/auth/selection/list/fetch.tsx @@ -0,0 +1,12 @@ +'use server'; +import { getAccessObjectField } from "@/fetchers/token/access"; +import { NextResponse } from "next/server"; + +async function getSelectionList() { + const selectionList = await getAccessObjectField("selectionList"); + if (!selectionList) return NextResponse.json({ success: false, message: "Selection list not found" }); + return NextResponse.json({ success: true, data: selectionList, message: "Selection list fetched successfully" }); +} + + +export { getSelectionList }; \ No newline at end of file diff --git a/ServicesFrontEnd/frontend/src/fetchers/base.ts b/ServicesFrontEnd/frontend/src/fetchers/base.ts new file mode 100644 index 0000000..48fe98f --- /dev/null +++ b/ServicesFrontEnd/frontend/src/fetchers/base.ts @@ -0,0 +1,52 @@ +import { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies"; +import NextCrypto from "next-crypto"; + +const tokenSecretEnv = process.env.TOKENSECRET_90; +const tokenSecret = tokenSecretEnv || "e781d1b0-9418-40b3-9940-385abf81a0b7"; +const REDIS_TIMEOUT = 5000; +const nextCrypto = new NextCrypto(tokenSecret); + +// Cookie options for cookies-next +const cookieOptions = { + httpOnly: true, + secure: false, + sameSite: "lax" as const, + path: "/", + maxAge: 30 * 60, // 30 minutes + // priority: "high", +}; + +const DEFAULT_TIMEOUT: number = 10000; +const defaultHeaders: Record = { + accept: "application/json", + language: "tr", + domain: "evyos.com.tr", + tz: "GMT+3", + "Content-type": "application/json", +}; + +// Cookie options for next/headers cookies +// const cookieObject: Partial = { +// httpOnly: true, +// path: "/", +// sameSite: "none", +// secure: true, +// maxAge: 1800, +// priority: "high", +// }; + +// const DEFAULT_RESPONSE: ApiResponse = { +// error: "Hata tipi belirtilmedi", +// status: 500, +// data: {}, +// }; + +export { + DEFAULT_TIMEOUT, + // DEFAULT_RESPONSE, + REDIS_TIMEOUT, + defaultHeaders, + tokenSecret, + cookieOptions, + nextCrypto, +}; diff --git a/ServicesFrontEnd/frontend/src/fetchers/fecther.ts b/ServicesFrontEnd/frontend/src/fetchers/fecther.ts new file mode 100644 index 0000000..f386cee --- /dev/null +++ b/ServicesFrontEnd/frontend/src/fetchers/fecther.ts @@ -0,0 +1,136 @@ +"use server"; +import { defaultHeaders, DEFAULT_TIMEOUT } from "./base"; +import { FetchOptions, HttpMethod, ApiResponse } from "./types"; +import { getPlainAccessToken } from "./token/access"; +import { getPlainSelectToken } from "./token/select"; + +const DEFAULT_RESPONSE = { + error: "Hata tipi belirtilmedi", + status: 500, + data: {}, +}; + +/** + * Creates a promise that rejects after a specified timeout + * @param ms Timeout in milliseconds + * @param controller AbortController to abort the fetch request + * @returns A promise that rejects after the timeout + */ +const createTimeoutPromise = ( + ms: number, + controller: AbortController +): Promise => { + return new Promise((_, reject) => { + setTimeout(() => { + controller.abort(); + reject(new Error(`Request timed out after ${ms}ms`)); + }, ms); + }); +}; + +/** + * Core fetch function with timeout and error handling + * @param url The URL to fetch + * @param options Fetch options + * @param headers Request headers + * @param payload Request payload + * @returns API response + */ +async function coreFetch( + url: string, + options: FetchOptions = {}, + headers: Record = defaultHeaders, + payload?: any +): Promise> { + const { method = "POST", cache = false, timeout = DEFAULT_TIMEOUT } = options; + + try { + const controller = new AbortController(); + const fetchOptions: RequestInit = { + method, + headers, + cache: cache ? "force-cache" : "no-cache", + signal: controller.signal, + }; + + if (method !== "GET" && payload) { + fetchOptions.body = JSON.stringify( + payload.payload ? payload.payload : payload + ); + } + const timeoutPromise = createTimeoutPromise(timeout, controller); + const response = await Promise.race([ + fetch(url, fetchOptions), + timeoutPromise, + ]); + const responseData = await response.json(); + return { + status: response.status, + data: responseData || ({} as T), + }; + } catch (error) { + console.error(`API Error (${url}):`, error); + return { + ...DEFAULT_RESPONSE, + error: error instanceof Error ? error.message : "Network error", + } as ApiResponse; + } +} + +/** + * Fetch data without authentication + */ +async function fetchData( + endpoint: string, + payload?: any, + method: HttpMethod = "POST", + cache: boolean = false, + timeout: number = DEFAULT_TIMEOUT +): Promise> { + return coreFetch( + endpoint, + { method, cache, timeout }, + defaultHeaders, + payload + ); +} + +/** + * Fetch data with authentication token + */ +async function fetchDataWithToken( + endpoint: string, + payload?: any, + method: HttpMethod = "POST", + cache: boolean = false, + timeout: number = DEFAULT_TIMEOUT +): Promise> { + const accessToken = await getPlainAccessToken(); + const selectToken = await getPlainSelectToken(); + const headers = { ...defaultHeaders, acs: accessToken, slc: selectToken }; + return coreFetch(endpoint, { method, cache, timeout }, headers, payload); +} + +/** + * Update data with authentication token and UUID + */ +async function updateDataWithToken( + endpoint: string, + uuid: string, + payload?: any, + method: HttpMethod = "POST", + cache: boolean = false, + timeout: number = DEFAULT_TIMEOUT +): Promise> { + const accessToken = await getPlainAccessToken(); + const selectToken = await getPlainSelectToken(); + const headers = { ...defaultHeaders, acs: accessToken, slc: selectToken }; + return coreFetch( + `${endpoint}/${uuid}`, + { method, cache, timeout }, + headers, + payload + ); +} + +export { fetchData, fetchDataWithToken, updateDataWithToken }; diff --git a/ServicesFrontEnd/frontend/src/libss/redis.ts b/ServicesFrontEnd/frontend/src/fetchers/redis/redis.ts similarity index 100% rename from ServicesFrontEnd/frontend/src/libss/redis.ts rename to ServicesFrontEnd/frontend/src/fetchers/redis/redis.ts diff --git a/ServicesFrontEnd/frontend/src/libss/redisService.ts b/ServicesFrontEnd/frontend/src/fetchers/redis/redisService.ts similarity index 73% rename from ServicesFrontEnd/frontend/src/libss/redisService.ts rename to ServicesFrontEnd/frontend/src/fetchers/redis/redisService.ts index 830befe..7dc171e 100644 --- a/ServicesFrontEnd/frontend/src/libss/redisService.ts +++ b/ServicesFrontEnd/frontend/src/fetchers/redis/redisService.ts @@ -1,4 +1,5 @@ import redis from "./redis"; +import { redisScanAccess, redisScanSelect } from "../types/base"; interface SScanParams { rKey: string; @@ -90,13 +91,34 @@ export async function deleteKey(params: GetParams): RDelete { } export async function scanByRKeySingle(params: SScanParams): RScan { - const pattern = `X:${params.rKey}:${params.rKey}:*:*`; - const [_, results] = await redis.scan("0", "MATCH", pattern, "COUNT", 1); - return results.length > 0 ? results[0] : null; + const pattern = redisScanAccess(params.rKey); + const keys: string[] = []; + let cursor = "0"; + do { + const [nextCursor, matchedKeys] = await redis.scan( + cursor, + "MATCH", + pattern + ); + cursor = nextCursor; + keys.push(...matchedKeys); + } while (cursor !== "0"); + return keys.length > 0 ? keys[0] : null; } export async function scanByRKeyDouble(params: DScanParams): RScan { - const pattern = `X:${params.rKey}:${params.sKey}:*:*`; - const [_, results] = await redis.scan("0", "MATCH", pattern, "COUNT", 1); - return results.length > 0 ? results[0] : null; + const pattern = redisScanSelect(params.rKey, params.sKey); + console.log("pattern", pattern); + const keys: string[] = []; + let cursor = "0"; + do { + const [nextCursor, matchedKeys] = await redis.scan( + cursor, + "MATCH", + pattern + ); + cursor = nextCursor; + keys.push(...matchedKeys); + } while (cursor !== "0"); + return keys.length > 0 ? keys[0] : null; } diff --git a/ServicesFrontEnd/frontend/src/fetchers/token/access.tsx b/ServicesFrontEnd/frontend/src/fetchers/token/access.tsx new file mode 100644 index 0000000..5b3e830 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/fetchers/token/access.tsx @@ -0,0 +1,100 @@ +'use server'; +import { AuthError } from "@/fetchers/types/base"; +import { scanByRKeySingle, getJSON } from "@/fetchers/redis/redisService"; +import { nextCrypto } from "@/fetchers/base"; +import { getCookieAccessToken, removeCookieTokens, setCookieAccessToken } from "./cookies"; + +async function getPlainAccessToken() { + try { + let encryptedAccessToken = ""; + const accessCookie = await getCookieAccessToken(); + if (accessCookie?.value) { + encryptedAccessToken = accessCookie.value; + } + if (!encryptedAccessToken) { + throw new AuthError("No access token found"); + } + try { + const decryptedAccessToken = await nextCrypto.decrypt(encryptedAccessToken) || ""; + if (!decryptedAccessToken) { + throw new AuthError("Access token is invalid"); + } + return decryptedAccessToken; + } catch (decryptError) { + throw new AuthError("Failed to decrypt access token"); + } + } + catch (error) { + throw new AuthError("No access token found"); + } +} + +async function getAccessToken() { + try { + const plainAccessToken = await getPlainAccessToken(); + if (!plainAccessToken) { + throw new AuthError("No access token found"); + } + try { + const scanToken = await scanByRKeySingle({ rKey: plainAccessToken }); + if (!scanToken) throw new AuthError("Access token is invalid"); + return scanToken; + } catch (scanError) { + throw new AuthError("Failed to validate access token"); + } + } + catch (error) { + throw new AuthError("No access token found"); + } +} + +async function getAccessObject() { + try { + const accessToken = await getAccessToken(); + if (!accessToken) { + throw new AuthError("No access token found"); + } + const accessObject = await getJSON({ key: accessToken }); + return accessObject; + } + catch (error) { + throw new AuthError("No access token found"); + } +} + +async function getAccessObjectField(field: string): Promise | null> { + try { + const accessToken = await getAccessToken(); + const accessObject = await getJSON({ key: accessToken }); + if (!accessObject) { + return null; + } else { + if (Object.keys(accessObject).includes(field)) { return accessObject[field as keyof typeof accessObject] } + } + } catch (error) { } + return null; +} + +async function isAccessTokenValid() { + try { + const accessToken = await getAccessObject(); + console.log('accessToken', accessToken); + if (accessToken) { + return true + } else { + removeCookieTokens(); + return false + } + } catch (error) { } + return false; +} + +async function setAccessToken(token: string) { + await setCookieAccessToken(token); +} + +async function removeAccessToken() { + await removeCookieTokens(); +} + +export { getAccessToken, setAccessToken, removeAccessToken, getPlainAccessToken, isAccessTokenValid, getAccessObject, getAccessObjectField }; diff --git a/ServicesFrontEnd/frontend/src/fetchers/token/cookies.tsx b/ServicesFrontEnd/frontend/src/fetchers/token/cookies.tsx new file mode 100644 index 0000000..081a3ae --- /dev/null +++ b/ServicesFrontEnd/frontend/src/fetchers/token/cookies.tsx @@ -0,0 +1,87 @@ +'use server'; +import { nextCrypto, cookieOptions } from "@/fetchers/base"; +import { cookies } from 'next/headers'; + +const ACCESS_TOKEN_COOKIE = 'acs'; +const SELECT_TOKEN_COOKIE = 'slc'; + +export async function getCookieAccessToken() { + try { + const cookieStore = await cookies(); + const encryptedToken = cookieStore.get(ACCESS_TOKEN_COOKIE); + if (!encryptedToken) { + return undefined; + } + return { name: ACCESS_TOKEN_COOKIE, value: encryptedToken.value }; + } catch (error) { + return undefined; + } +} + +export async function setCookieAccessToken(token: string) { + try { + const encryptedToken = await nextCrypto.encrypt(token); + const cookieStore = await cookies(); + cookieStore.set(ACCESS_TOKEN_COOKIE, encryptedToken, { + httpOnly: cookieOptions.httpOnly, + secure: cookieOptions.secure, + sameSite: cookieOptions.sameSite, + path: cookieOptions.path, + maxAge: cookieOptions.maxAge + }); + } catch (error) { + } +} + +export async function getCookieSelectToken() { + try { + const cookieStore = await cookies(); + const encryptedToken = cookieStore.get(SELECT_TOKEN_COOKIE); + if (!encryptedToken) { + return undefined; + } + return { name: SELECT_TOKEN_COOKIE, value: encryptedToken.value }; + } catch (error) { + return undefined; + } +} + +export async function setCookieSelectToken(token: string) { + try { + const encryptedToken = await nextCrypto.encrypt(token); + const cookieStore = await cookies(); + cookieStore.set(SELECT_TOKEN_COOKIE, encryptedToken, { + httpOnly: cookieOptions.httpOnly, + secure: cookieOptions.secure, + sameSite: cookieOptions.sameSite, + path: cookieOptions.path, + maxAge: cookieOptions.maxAge + }); + } catch (error) { + } +} + +export async function removeAccessToken() { + try { + const cookieStore = await cookies(); + cookieStore.delete(ACCESS_TOKEN_COOKIE); + } catch (error) { + } +} + +export async function removeSelectToken() { + try { + const cookieStore = await cookies(); + cookieStore.delete(SELECT_TOKEN_COOKIE); + } catch (error) { + } +} + +export async function removeCookieTokens() { + try { + const cookieStore = await cookies(); + cookieStore.delete(ACCESS_TOKEN_COOKIE); + cookieStore.delete(SELECT_TOKEN_COOKIE); + } catch (error) { + } +} diff --git a/ServicesFrontEnd/frontend/src/fetchers/token/select.tsx b/ServicesFrontEnd/frontend/src/fetchers/token/select.tsx new file mode 100644 index 0000000..12a7d91 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/fetchers/token/select.tsx @@ -0,0 +1,78 @@ +'use server'; +import { AuthError } from "@/fetchers/types/base"; +import { scanByRKeyDouble } from "@/fetchers/redis/redisService"; +import { getPlainAccessToken } from "./access"; +import { nextCrypto } from "@/fetchers/base"; +import { getCookieSelectToken, removeCookieTokens, setCookieSelectToken } from "./cookies"; + +async function getPlainSelectToken() { + console.log('getPlainSelectToken is triggered...'); + + try { + let encryptedSelectToken = ""; + + // Try to get token from cookies first + const selectCookie = await getCookieSelectToken(); + if (selectCookie?.value) { + encryptedSelectToken = selectCookie.value; + console.log('Select token from cookie:', 'found'); + } + + console.log('getPlainSelectToken encryptedSelectToken', encryptedSelectToken ? `${encryptedSelectToken.substring(0, 10)}...` : 'empty'); + + if (!encryptedSelectToken) { + console.log('No select token found'); + throw new AuthError("No select token found"); + } + + const decryptedSelectToken = await nextCrypto.decrypt(encryptedSelectToken) || ""; + console.log('getPlainSelectToken decryptedSelectToken', decryptedSelectToken ? `${decryptedSelectToken.substring(0, 10)}...` : 'empty'); + + if (!decryptedSelectToken) { + console.log('Decrypted select token is invalid or empty'); + throw new AuthError("Select token is invalid"); + } + + return decryptedSelectToken; + } + catch (error) { + console.error('Error in getPlainSelectToken:', error); + throw new AuthError("No select token found on cookies"); + } +} + +async function getSelectToken() { + try { + const plainAccessToken = await getPlainAccessToken(); + const plainSelectToken = await getPlainSelectToken(); + console.log('plainAccessToken', plainAccessToken); + console.log('plainSelectToken', plainSelectToken); + const scanToken = await scanByRKeyDouble({ rKey: plainAccessToken, sKey: plainSelectToken }); + if (!scanToken) throw new AuthError("Select token is invalid"); + return scanToken; + } + catch (error) { throw new AuthError("No select token found in headers") } +} + +async function setSelectToken(token: string) { + console.log('setSelectToken is triggered...'); + await setCookieSelectToken(token); +} + +async function isSelectTokenValid() { + try { + const selectToken = await getSelectToken(); + console.log('isSelectTokenValid selectToken', selectToken); + if (selectToken) return true; + } catch (error) { + console.log('isSelectTokenValid error', error); + return false + } +} + +async function removeSelectToken() { + console.log('removeSelectToken is triggered'); + await removeCookieTokens(); +} + +export { getSelectToken, setSelectToken, removeSelectToken, getPlainSelectToken, isSelectTokenValid }; diff --git a/ServicesFrontEnd/frontend/src/fetchers/types.ts b/ServicesFrontEnd/frontend/src/fetchers/types.ts new file mode 100644 index 0000000..27ed5e3 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/fetchers/types.ts @@ -0,0 +1,32 @@ +type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + +interface ApiResponse { + status: number; + data: T; + error?: string; +} + +interface FetchOptions { + method?: HttpMethod; + cache?: boolean; + timeout?: number; +} + +interface CookieObject { + httpOnly: boolean; + path: string; + sameSite: string; + secure: boolean; + maxAge: number; + priority: string; +} + +interface Pagination { + page?: number; + size?: number; + orderField?: string[]; + orderType?: string[]; + query?: Record; +} + +export type { HttpMethod, ApiResponse, FetchOptions, CookieObject, Pagination }; diff --git a/ServicesFrontEnd/frontend/src/fetchers/types/base.ts b/ServicesFrontEnd/frontend/src/fetchers/types/base.ts new file mode 100644 index 0000000..2c4212d --- /dev/null +++ b/ServicesFrontEnd/frontend/src/fetchers/types/base.ts @@ -0,0 +1,17 @@ +export class AuthError extends Error { + constructor(message: string) { + super(message); + this.name = "AuthError"; + } +} + +const accessExt = "AUTH_TOKEN"; +const selectExt = "SELECT_TOKEN"; +function redisScanAccess(cookieToken: string) { + return `${accessExt}:${cookieToken}:${cookieToken}:*:*`; +} +function redisScanSelect(cookieToken: string, selectToken: string) { + return `${selectExt}:${cookieToken}:${selectToken}:*:*`; +} + +export { redisScanAccess, redisScanSelect }; diff --git a/ServicesFrontEnd/frontend/src/fetchers/types/login/validations.ts b/ServicesFrontEnd/frontend/src/fetchers/types/login/validations.ts new file mode 100644 index 0000000..b0deddc --- /dev/null +++ b/ServicesFrontEnd/frontend/src/fetchers/types/login/validations.ts @@ -0,0 +1,11 @@ +interface LoginViaAccessKeys { + accessKey: string; + password: string; + rememberMe: boolean; +} + +interface LoginSelect { + uuid: string; +} + +export type { LoginViaAccessKeys, LoginSelect }; diff --git a/ServicesFrontEnd/frontend/src/fetchers/urls.ts b/ServicesFrontEnd/frontend/src/fetchers/urls.ts new file mode 100644 index 0000000..6e1303f --- /dev/null +++ b/ServicesFrontEnd/frontend/src/fetchers/urls.ts @@ -0,0 +1,6 @@ +const baseUrl = process.env.API_URL || "http://localhost:8001"; +const selfApi = process.env.SELF_API_URL || "http://localhost:3000"; +const urlLoginEndpoint = `${baseUrl}/auth/login`; +const urlSelectEndpoint = `${baseUrl}/auth/select`; + +export { urlLoginEndpoint, urlSelectEndpoint, selfApi }; diff --git a/ServicesFrontEnd/frontend/src/i18n/en.json b/ServicesFrontEnd/frontend/src/i18n/en.json index 7525bfb..b47299c 100644 --- a/ServicesFrontEnd/frontend/src/i18n/en.json +++ b/ServicesFrontEnd/frontend/src/i18n/en.json @@ -12,7 +12,9 @@ "home": "Home", "about": "About", "contact": "Contact", - "current": "Current Language" + "current": "Current Language", + "login": "Login", + "select": "Select" } }, "LocaleLayout": { @@ -41,6 +43,52 @@ "signUp": "Sign up", "copyright": "© 2023 Your Company. All rights reserved.", "welcomeBack": "Welcome Back", - "continueJourney": "Sign in to continue your journey with us" + "continueJourney": "Sign in to continue your journey with us", + "emailWrong": "Please check email format", + "passwordWrong": "Please check password format" + }, + "Select": { + "title": "Select Your Option", + "description": "Please select one of the following options to continue", + "employee": "Employee", + "occupant_type": "Occupant Type", + "staff": "Staff", + "uuid": "UUID", + "department": "Department", + "name": "Name", + "code": "Code", + "company": "Company", + "occupant": "Occupant", + "occupant_code": "Occupant Code", + "building": "Building", + "type": "Type", + "part_details": "Part Details", + "no": "No", + "level": "Level", + "status": "Status", + "livable": "Livable", + "not_livable": "Not Livable", + "selection": "Selection", + "id": "ID", + "processing": "Processing...", + "continue": "Continue", + "select_option": "Select an option to continue", + "recentLogins": "Recent logins", + "clickPictureOrAdd": "Click your picture or add an account", + "addAccount": "Add an account", + "show": "Show", + "hide": "Hide", + "signInWithGoogle": "Sign in with Google", + "signInWithFacebook": "Sign in with Facebook", + "enterCredentials": "Enter your credentials to continue", + "rememberMe": "Remember me", + "orContinueWith": "or continue with", + "noAccount": "Don't have an account?", + "signUp": "Sign up", + "copyright": "© 2023 Your Company. All rights reserved.", + "welcomeBack": "Welcome Back", + "continueJourney": "Sign in to continue your journey with us", + "emailWrong": "Please check email format", + "passwordWrong": "Please check password format" } } diff --git a/ServicesFrontEnd/frontend/src/i18n/routing.ts b/ServicesFrontEnd/frontend/src/i18n/routing.ts index ed195de..e4cc101 100644 --- a/ServicesFrontEnd/frontend/src/i18n/routing.ts +++ b/ServicesFrontEnd/frontend/src/i18n/routing.ts @@ -74,7 +74,7 @@ export const usePathname = () => { const locale = params?.locale as Locale; // Remove locale prefix from pathname - if (locale && nextPathname.startsWith(`/${locale}`)) { + if (locale && nextPathname && nextPathname.startsWith(`/${locale}`)) { return nextPathname.substring(`/${locale}`.length) || "/"; } diff --git a/ServicesFrontEnd/frontend/src/i18n/tr.json b/ServicesFrontEnd/frontend/src/i18n/tr.json index 2e63bcf..e8ae0ac 100644 --- a/ServicesFrontEnd/frontend/src/i18n/tr.json +++ b/ServicesFrontEnd/frontend/src/i18n/tr.json @@ -12,7 +12,9 @@ "home": "Ana Sayfa", "about": "Hakkında", "contact": "İletişim", - "current": "Mevcut Dili" + "current": "Mevcut Dili", + "login": "Giriş Yap", + "select": "Görev Seç" } }, "LocaleLayout": { @@ -41,6 +43,52 @@ "signUp": "Kayıt ol", "copyright": "© 2023 Şirketiniz. Tüm hakları saklıdır.", "welcomeBack": "Tekrar Hoşgeldiniz", - "continueJourney": "Yolculuğunuza devam etmek için giriş yapın" + "continueJourney": "Yolculuğunuza devam etmek için giriş yapın", + "emailWrong": "Emaili tekrar kontrol edin", + "passwordWrong": "Parolayı tekrar kontrol edin" + }, + "Select": { + "title": "Seçeneğinizi Seçin", + "description": "Devam etmek için lütfen aşağıdaki seçeneklerden birini seçin", + "employee": "Çalışan", + "staff": "Personel", + "occupant_type": "İkamet Tipi", + "uuid": "UUID", + "department": "Departman", + "name": "İsim", + "code": "Kod", + "company": "Şirket", + "occupant": "Oturak", + "occupant_code": "Oturak Kodu", + "building": "Bina", + "type": "Tip", + "part_details": "Parça Detayları", + "no": "No", + "level": "Seviye", + "status": "Durum", + "livable": "Yaşanabilir", + "not_livable": "Yaşanamaz", + "selection": "Seçim", + "id": "ID", + "processing": "İşleniyor...", + "continue": "Devam Et", + "select_option": "Devam etmek için bir seçenek seçin", + "recentLogins": "Son girişler", + "clickPictureOrAdd": "Resminize tıklayın veya bir hesap ekleyin", + "addAccount": "Hesap ekle", + "show": "Göster", + "hide": "Gizle", + "signInWithGoogle": "Google ile giriş yap", + "signInWithFacebook": "Facebook ile giriş yap", + "enterCredentials": "Devam etmek için kimlik bilgilerinizi girin", + "rememberMe": "Beni hatırla", + "orContinueWith": "veya şununla devam et", + "noAccount": "Hesabınız yok mu?", + "signUp": "Kayıt ol", + "copyright": " 2023 Şirketiniz. Tüm hakları saklıdır.", + "welcomeBack": "Tekrar Hoş Geldiniz", + "continueJourney": "Bizimle yolculuğunuza devam etmek için giriş yapın", + "emailWrong": "Lütfen e-posta formatını kontrol edin", + "passwordWrong": "Lütfen şifre formatını kontrol edin" } } diff --git a/ServicesFrontEnd/frontend/src/lib/fetcher.tsx b/ServicesFrontEnd/frontend/src/lib/fetcher.tsx index 016e285..f994344 100644 --- a/ServicesFrontEnd/frontend/src/lib/fetcher.tsx +++ b/ServicesFrontEnd/frontend/src/lib/fetcher.tsx @@ -1,3 +1,5 @@ +'use client'; + interface FetcherRequest { url: string; isNoCache: boolean; @@ -38,16 +40,23 @@ async function apiPostFetcher({ body, }: PostFetcherRequest): Promise> { try { + let headers: Record = { + "Content-Type": "application/json", + "no-cache": isNoCache ? "true" : "false" + }; const response = await fetch(url, { method: "POST", - headers: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" }, + headers, body: JSON.stringify(body), + credentials: 'include' }) if (response.status === 200) { const result = await response.json(); return { success: true, data: result.data, pagination: result.pagination } } - } catch (error) { } + } catch (error) { + console.error('API Post Fetcher error:', error); + } return { success: false, data: null } } @@ -57,16 +66,37 @@ async function apiGetFetcher({ isNoCache, }: GetFetcherRequest): Promise> { try { + let headers: Record = { + "Content-Type": "application/json", + "no-cache": isNoCache ? "true" : "false" + }; const response = await fetch(url, { method: "GET", - headers: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" }, + headers, + credentials: 'include' }) - console.log("apiGetFetcher status", await response.text()); - if (response.status === 200) { - const result = await response.json(); - return { success: true, data: result.data } + let responseText; + try { + responseText = await response.text(); + console.log("apiGetFetcher status", response.status, responseText.substring(0, 100)); + } catch (e) { + console.error("Error reading response text", e); } - } catch (error) { } + if (response.status === 200) { + let result; + try { + if (responseText) { + result = JSON.parse(responseText); + return { success: true, data: result.data }; + } + } catch (e) { + console.error("Error parsing JSON response", e); + } + return { success: false, data: null }; + } + } catch (error) { + console.error('API Get Fetcher error:', error); + } return { success: false, data: null } } @@ -76,18 +106,23 @@ async function apiDeleteFetcher({ isNoCache, }: DeleteFetcherRequest): Promise> { try { + let headers: Record = { + "Content-Type": "application/json", + "no-cache": isNoCache ? "true" : "false" + }; const response = await fetch(url, { method: "DELETE", - headers: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" }, + headers, + credentials: 'include' }) if (response.status === 200) { + const result = await response.json(); - return { - success: true, - data: result.data, - } + return { success: true, data: result.data } } - } catch (error) { } + } catch (error) { + console.error('API Delete Fetcher error:', error); + } return { success: false, data: null } } @@ -98,10 +133,15 @@ async function apiPutFetcher({ body, }: PutFetcherRequest): Promise> { try { + let headers: Record = { + "Content-Type": "application/json", + "no-cache": isNoCache ? "true" : "false" + }; const response = await fetch(url, { method: "PUT", - headers: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" }, + headers, body: JSON.stringify(body), + credentials: 'include' }) if (response.status === 200) { const result = await response.json(); @@ -121,10 +161,15 @@ async function apiPatchFetcher({ body, }: PatchFetcherRequest): Promise> { try { + let headers: Record = { + "Content-Type": "application/json", + "no-cache": isNoCache ? "true" : "false" + }; const response = await fetch(url, { method: "PATCH", - headers: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" }, + headers, body: JSON.stringify(body), + credentials: 'include' }) if (response.status === 200) { const result = await response.json(); diff --git a/ServicesFrontEnd/frontend/src/messages/en.json b/ServicesFrontEnd/frontend/src/messages/en.json deleted file mode 100644 index 608b39e..0000000 --- a/ServicesFrontEnd/frontend/src/messages/en.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Welcome Ninja" -} diff --git a/ServicesFrontEnd/frontend/src/messages/tr.json b/ServicesFrontEnd/frontend/src/messages/tr.json deleted file mode 100644 index 48e5bfe..0000000 --- a/ServicesFrontEnd/frontend/src/messages/tr.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Hoşgeldiniz Ninja" -} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1267d5e..4b967ea 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -6,7 +6,7 @@ services: dockerfile: ServicesApi/Dockerfile target: builder ports: - - "3000:3000" + - "8001:8001" env_file: - api_env.env environment: