diff --git a/README.md b/README.md index 85afb68..55c4fa5 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,14 @@ npx prisma db push # update remote schema # not good for production creates no s npx prisma validate npx prisma format + +# Frontend + +npx create-next-app@latest + +!npm install next-intl now towking with latest next js +npm install --save nestjs-i18n +npm install ioredis +npm install -D daisyui@latest +npm install tailwindcss @tailwindcss/postcss daisyui@latest +npm install lucide-react diff --git a/ServicesApi/prisma/schema.prisma b/ServicesApi/prisma/schema.prisma index 959d5b6..b178116 100644 --- a/ServicesApi/prisma/schema.prisma +++ b/ServicesApi/prisma/schema.prisma @@ -3508,6 +3508,7 @@ model staff { /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model users { user_tag String @default("") @db.VarChar(64) + user_type String @default("employee") @db.VarChar(32) email String @default("") @db.VarChar(128) phone_number String @default("") @db.VarChar via String @default("111") @db.VarChar diff --git a/ServicesApi/src/auth/auth.controller.ts b/ServicesApi/src/auth/auth.controller.ts index 570f3b0..381965d 100644 --- a/ServicesApi/src/auth/auth.controller.ts +++ b/ServicesApi/src/auth/auth.controller.ts @@ -31,8 +31,8 @@ export class AuthController { @Post('select') @HttpCode(200) @UseGuards(AuthControlGuard) - async select(@Body() query: userSelectValidator) { - return { message: 'Logout successful' }; + async select(@Body() query: userSelectValidator, @Req() req: Request) { + return await this.authService.select(query, req); } @Post('/password/create') diff --git a/ServicesApi/src/auth/auth.service.ts b/ServicesApi/src/auth/auth.service.ts index b72b656..83e1a7a 100644 --- a/ServicesApi/src/auth/auth.service.ts +++ b/ServicesApi/src/auth/auth.service.ts @@ -32,8 +32,8 @@ export class AuthService { return await this.logoutService.run(dto); } - async select(dto: userSelectValidator) { - return await this.selectService.run(dto); + async select(dto: userSelectValidator, req: Request) { + return await this.selectService.run(dto, req); } async createPassword(dto: any) { diff --git a/ServicesApi/src/auth/login/login.service.ts b/ServicesApi/src/auth/login/login.service.ts index b081b6f..bcdf499 100644 --- a/ServicesApi/src/auth/login/login.service.ts +++ b/ServicesApi/src/auth/login/login.service.ts @@ -18,9 +18,9 @@ export class LoginService { where: { email: dto.accessKey }, }); - // if (foundUser.password_token) { - // throw new Error('Password need to be set first'); - // } + if (foundUser.password_token) { + throw new Error('Password need to be set first'); + } const isPasswordValid = this.passHandlers.check_password( foundUser.uu_id, @@ -28,14 +28,13 @@ export class LoginService { foundUser.hash_password, ); - // if (!isPasswordValid) { - // throw new Error('Invalid 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, @@ -43,8 +42,8 @@ export class LoginService { person_id: foundPerson.id, person_name: foundPerson.firstname, }, + selectionList: [], }); - const accessToken = await this.redis.setLoginToRedis( redisData, foundUser.uu_id, diff --git a/ServicesApi/src/auth/password/verify-otp/verify-otp.service.ts b/ServicesApi/src/auth/password/verify-otp/verify-otp.service.ts index 01a275d..2dfcf60 100644 --- a/ServicesApi/src/auth/password/verify-otp/verify-otp.service.ts +++ b/ServicesApi/src/auth/password/verify-otp/verify-otp.service.ts @@ -29,5 +29,4 @@ export class VerifyOtpService { // // controller veya resolver içinden // const { secret, otpauthUrl } = authService.generate2FASecret('mehmet'); // const qrCodeImage = await authService.generateQRCode(otpauthUrl); - // // qrCodeImage → frontend’e gönder, diye gösterilebilir diff --git a/ServicesApi/src/auth/select/select.service.ts b/ServicesApi/src/auth/select/select.service.ts index f0c31ab..6187ab2 100644 --- a/ServicesApi/src/auth/select/select.service.ts +++ b/ServicesApi/src/auth/select/select.service.ts @@ -1,9 +1,124 @@ -import { Injectable } from '@nestjs/common'; +import { + Injectable, + BadRequestException, + UnauthorizedException, + NotAcceptableException, +} from '@nestjs/common'; import { userSelectValidator } from '@/src/auth/select/dtoValidator'; +import { RedisHandlers } from '@/src/utils/auth/redis_handlers'; +import { + EmployeeTokenSchema, + OccupantTokenSchema, + TokenDictInterface, + UserType, +} from '@/src/types/auth/token'; +import { PrismaService } from '@/src/prisma.service'; @Injectable() export class SelectService { - async run(dto: userSelectValidator) { - return dto; + constructor( + private readonly redis: RedisHandlers, + private readonly prisma: PrismaService, + ) {} + async run(dto: userSelectValidator, req: Request) { + const accessObject = await this.redis.getLoginFromRedis(req); + if (!accessObject) { + throw new UnauthorizedException( + 'Authorization failed. Please login to continue', + ); + } + const accessToken = accessObject.key.split(':')[1]; + console.log('accessToken', accessToken); + + const userType = accessObject.value.users.user_type; + if (userType === 'employee') { + const employee = await this.prisma.employees.findFirstOrThrow({ + where: { uu_id: dto.selected_uu_id }, + }); + const staff = await this.prisma.staff.findFirstOrThrow({ + where: { id: employee.staff_id }, + }); + const duties = await this.prisma.duties.findFirstOrThrow({ + where: { id: staff.duties_id }, + }); + const department = await this.prisma.departments.findFirstOrThrow({ + where: { id: duties.department_id }, + }); + const duty = await this.prisma.duty.findFirstOrThrow({ + where: { id: duties.duties_id }, + }); + const company = await this.prisma.companies.findFirstOrThrow({ + where: { id: duties.company_id }, + }); + + const employeeToken = EmployeeTokenSchema.parse({ + company: company, + department: department, + duty: duty, + employee: employee, + staff: staff, + menu: null, + pages: null, + config: null, + caches: null, + selection: null, + functionsRetriever: staff.function_retriever, + kind: UserType.employee, + }); + + const tokenSelect = await this.redis.setSelectToRedis( + accessToken, + employeeToken, + accessObject.value.users.uu_id, + dto.selected_uu_id, + ); + + return { + message: 'Select successful', + token: tokenSelect, + }; + } else if (userType === 'occupant') { + const livingSpace = await this.prisma.build_living_space.findFirstOrThrow( + { where: { uu_id: dto.selected_uu_id } }, + ); + const occupantType = await this.prisma.occupant_types.findFirstOrThrow({ + where: { id: livingSpace.occupant_type_id }, + }); + const part = await this.prisma.build_parts.findFirstOrThrow({ + where: { id: livingSpace.build_parts_id }, + }); + const build = await this.prisma.build.findFirstOrThrow({ + where: { id: part.build_id }, + }); + const company = await this.prisma.companies.findFirstOrThrow({ + where: { uu_id: accessObject.value.users.related_company }, + }); + const occupantToken = OccupantTokenSchema.parse({ + livingSpace: livingSpace, + occupant: occupantType, + build: build, + part: part, + company: company, + menu: null, + pages: null, + config: null, + caches: null, + selection: null, + functionsRetriever: occupantType.function_retriever, + kind: UserType.occupant, + }); + const tokenSelect = await this.redis.setSelectToRedis( + accessToken, + occupantToken, + accessObject.value.users.uu_id, + dto.selected_uu_id, + ); + return { + message: 'Select successful', + token: tokenSelect, + }; + } else { + throw new NotAcceptableException('Invalid user type'); + } } } diff --git a/ServicesApi/src/middleware/access-control.guard.ts b/ServicesApi/src/middleware/access-control.guard.ts index 0cce8ba..44ea175 100644 --- a/ServicesApi/src/middleware/access-control.guard.ts +++ b/ServicesApi/src/middleware/access-control.guard.ts @@ -14,14 +14,6 @@ export class AuthControlGuard implements CanActivate { const req = context.switchToHttp().getRequest(); const accessToken = this.cacheService.mergeLoginKey(req); console.log('AuthControlGuard', accessToken); - // const hasAccess = accessObject.permissions?.some( - // (p: any) => p.method === method && p.url === path, - // ); - - // if (!hasAccess) { - // throw new ForbiddenException('Access denied to this route'); - // } - return true; } } @@ -32,18 +24,11 @@ export class EndpointControlGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const req = context.switchToHttp().getRequest(); - const selectToken = this.cacheService.mergeSelectKey(req); - const method = req.method; - const path = req.route?.path; - console.log('EndpointControlGuard', selectToken, method, path); - // const hasAccess = accessObject.permissions?.some( - // (p: any) => p.method === method && p.url === path, - // ); - - // if (!hasAccess) { - // throw new ForbiddenException('Access denied to this route'); - // } - + // const selectToken = this.cacheService.mergeSelectKey(req); + // const method = req.method; + // const path = req.route?.path; + const accessObject = await this.cacheService.getSelectFromRedis(req); + console.log('EndpointControlGuard', accessObject); return true; } } diff --git a/ServicesApi/src/types/auth/token.ts b/ServicesApi/src/types/auth/token.ts index dbf6959..e97ccb4 100644 --- a/ServicesApi/src/types/auth/token.ts +++ b/ServicesApi/src/types/auth/token.ts @@ -26,7 +26,7 @@ export const AuthTokenSchema = z.object({ father_name: z.string(), mother_name: z.string(), country_code: z.string(), - national_identity_id: z.string(), + // national_identity_id: z.string(), birth_place: z.string(), birth_date: z.date(), tax_no: z.string(), @@ -52,10 +52,11 @@ export const AuthTokenSchema = z.object({ users: z.object({ user_tag: z.string(), email: z.string(), + user_type: z.string(), phone_number: z.string(), via: z.string(), avatar: z.string(), - hash_password: z.string(), + // hash_password: z.string(), password_token: z.string(), remember_me: z.boolean(), password_expires_day: z.number(), @@ -85,33 +86,173 @@ export const AuthTokenSchema = z.object({ default_language: z.string(), }), credentials: CredentialsSchema, + selectionList: z.array(z.any()).optional().default([]), }); export type AuthToken = z.infer; export const EmployeeTokenSchema = z.object({ + company: z.object({ + id: z.number(), + uu_id: z.string(), + formal_name: z.string(), + company_type: z.string(), + commercial_type: z.string(), + tax_no: z.string(), + public_name: z.string(), + company_tag: z.string(), + default_lang_type: z.string(), + default_money_type: z.string(), + is_commercial: z.boolean(), + is_blacklist: z.boolean(), + parent_id: z.number().nullable(), + workplace_no: z.string().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(), + created_credentials_token: z.string().nullable(), + updated_credentials_token: z.string().nullable(), + confirmed_credentials_token: z.string().nullable(), + is_confirmed: z.boolean(), + deleted: z.boolean(), + active: z.boolean(), + is_notification_send: z.boolean(), + is_email_send: z.boolean(), + expiry_starts: z.date(), + expiry_ends: z.date(), + created_at: z.date(), + updated_at: z.date(), + ref_int: z.number().nullable(), + }), + department: z.object({ + 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_uu_id: z.string(), + 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(), + is_confirmed: z.boolean(), + deleted: z.boolean(), + active: z.boolean(), + is_notification_send: z.boolean(), + is_email_send: z.boolean(), + expiry_starts: z.date(), + expiry_ends: z.date(), + created_at: z.date(), + updated_at: z.date(), + ref_int: z.number().nullable(), + }), + duty: z.object({ + 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(), + created_credentials_token: z.string().nullable(), + updated_credentials_token: z.string().nullable(), + confirmed_credentials_token: z.string().nullable(), + is_confirmed: z.boolean(), + deleted: z.boolean(), + active: z.boolean(), + is_notification_send: z.boolean(), + is_email_send: z.boolean(), + expiry_starts: z.date(), + expiry_ends: z.date(), + created_at: z.date(), + updated_at: z.date(), + ref_int: z.number().nullable(), + }), + employee: z.object({ + 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(), + created_credentials_token: z.string().nullable(), + updated_credentials_token: z.string().nullable(), + confirmed_credentials_token: z.string().nullable(), + is_confirmed: z.boolean(), + deleted: z.boolean(), + active: z.boolean(), + is_notification_send: z.boolean(), + is_email_send: z.boolean(), + expiry_starts: z.date(), + expiry_ends: z.date(), + created_at: z.date(), + updated_at: z.date(), + ref_int: z.number().nullable(), + }), + staff: z.object({ + id: z.number(), + uu_id: z.string(), + staff_description: z.string(), + staff_name: z.string(), + staff_code: z.string(), + 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(), + created_credentials_token: z.string().nullable(), + updated_credentials_token: z.string().nullable(), + confirmed_credentials_token: z.string().nullable(), + is_confirmed: z.boolean(), + deleted: z.boolean(), + active: z.boolean(), + is_notification_send: z.boolean(), + is_email_send: z.boolean(), + expiry_starts: z.date(), + expiry_ends: z.date(), + created_at: z.date(), + updated_at: z.date(), + ref_int: z.number().nullable(), + }), + + 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(), - companies: z.object({}), - department: z.object({}), - duties: z.object({}), - employee: z.object({}), - staffs: z.object({}), - reachable_event_codes: z.array(z.object({})), - reachable_app_codes: z.array(z.object({})), kind: z.literal(UserType.employee), }); export const OccupantTokenSchema = z.object({ - functionsRetriever: z.string(), livingSpace: z.object({}), - occupantType: z.object({}), + occupant: z.object({}), build: z.object({}), - buildPart: z.object({}), - responsibleCompany: z.object({}).optional(), - responsibleEmployee: z.object({}).optional(), + part: z.object({}), + company: z.object({}).optional(), + + 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(), kind: z.literal(UserType.occupant), - reachable_event_codes: z.array(z.object({})), - reachable_app_codes: z.array(z.object({})), }); export const TokenDictTypes = z.discriminatedUnion('kind', [ diff --git a/ServicesApi/src/utils/auth/redis_handlers.ts b/ServicesApi/src/utils/auth/redis_handlers.ts index 9e7e93d..855249c 100644 --- a/ServicesApi/src/utils/auth/redis_handlers.ts +++ b/ServicesApi/src/utils/auth/redis_handlers.ts @@ -2,10 +2,9 @@ import { TokenDictTypes, TokenDictInterface, AuthToken, - UserType, + AuthTokenSchema, } from '@/src/types/auth/token'; import { CacheService } from '@/src/cache.service'; -import { users } from '@prisma/client'; import { PasswordHandlers } from './login_handler'; import { Injectable, ForbiddenException } from '@nestjs/common'; @@ -22,6 +21,7 @@ interface SelectFromRedis { @Injectable() export class RedisHandlers { AUTH_TOKEN = 'AUTH_TOKEN'; + SELECT_TOKEN = 'SELECT_TOKEN'; constructor( private readonly cacheService: CacheService, private readonly passwordService: PasswordHandlers, @@ -32,10 +32,10 @@ export class RedisHandlers { * Format: AUTH_TOKEN:token:token:UUID or AUTH_TOKEN:token:token:*:* this.AUTH_TOKEN:token:token:UUID:UUID */ - private validateRedisKey(redisKey: string): boolean { - if (!redisKey.startsWith(this.AUTH_TOKEN + ':')) { + private validateRedisKey(redisKey: string, type: string): boolean { + if (!redisKey.startsWith(type + ':')) { throw new ForbiddenException( - `Invalid Redis key format. Must start with ${this.AUTH_TOKEN}:`, + `Invalid Redis key format. Must start with ${type}:`, ); } const colonCount = (redisKey.match(/:/g) || []).length; @@ -53,7 +53,7 @@ export class RedisHandlers { throw new ForbiddenException('Access token header is missing'); } const mergedRedisKey = `${this.AUTH_TOKEN}:${acsToken}:${acsToken}:*:*`; - this.validateRedisKey(mergedRedisKey); + this.validateRedisKey(mergedRedisKey, this.AUTH_TOKEN); return mergedRedisKey; } @@ -66,8 +66,8 @@ export class RedisHandlers { if (!slcToken) { throw new ForbiddenException('Select token header is missing'); } - const mergedRedisKey = `${this.AUTH_TOKEN}:${acsToken}:${slcToken}:*:*`; - this.validateRedisKey(mergedRedisKey); + const mergedRedisKey = `${this.SELECT_TOKEN}:${acsToken}:${slcToken}:*:*`; + this.validateRedisKey(mergedRedisKey, this.SELECT_TOKEN); return mergedRedisKey; } @@ -79,8 +79,8 @@ export class RedisHandlers { return this.passwordService.generateAccessToken(); } - private async scanKeys(pattern: string): Promise { - this.validateRedisKey(pattern); + private async scanKeys(pattern: string, type: string): Promise { + this.validateRedisKey(pattern, type); const client = (this.cacheService as any).client; if (!client) throw new Error('Redis client not available'); @@ -103,7 +103,7 @@ export class RedisHandlers { async getLoginFromRedis(req: Request): Promise { const mergedKey = this.mergeLoginKey(req); if (mergedKey.includes('*')) { - const keys = await this.scanKeys(mergedKey); + const keys = await this.scanKeys(mergedKey, this.AUTH_TOKEN); if (keys.length === 0) { throw new ForbiddenException('Authorization failed - No matching keys'); } @@ -122,13 +122,15 @@ export class RedisHandlers { } const value = await this.cacheService.get(mergedKey); - return value ? { key: mergedKey, value } : null; + return value + ? { key: mergedKey, value: AuthTokenSchema.parse(value) } + : null; } async getSelectFromRedis(req: Request): Promise { const mergedKey = this.mergeSelectKey(req); if (mergedKey.includes('*')) { - const keys = await this.scanKeys(mergedKey); + const keys = await this.scanKeys(mergedKey, this.SELECT_TOKEN); if (keys.length === 0) { throw new ForbiddenException( @@ -147,7 +149,9 @@ export class RedisHandlers { } const value = await this.cacheService.get(mergedKey); - return value ? { key: mergedKey, value } : null; + return value + ? { key: mergedKey, value: TokenDictTypes.parse(value) } + : null; } async deleteLoginFromRedis(req: Request): Promise { @@ -186,7 +190,7 @@ export class RedisHandlers { livingUUID: string, ): Promise { const selectToken = this.generateSelectToken(accessToken, userUUID); - const redisKey = `${this.AUTH_TOKEN}:${accessToken}:${selectToken}:${userUUID}:${livingUUID}`; + const redisKey = `${this.SELECT_TOKEN}:${accessToken}:${selectToken}:${userUUID}:${livingUUID}`; await this.cacheService.set_with_ttl(redisKey, token, 60 * 30); return selectToken; } diff --git a/ServicesFrontEnd/frontend/package-lock.json b/ServicesFrontEnd/frontend/package-lock.json index 1ce5648..8accf7c 100644 --- a/ServicesFrontEnd/frontend/package-lock.json +++ b/ServicesFrontEnd/frontend/package-lock.json @@ -9,17 +9,21 @@ "version": "0.1.0", "dependencies": { "clsx": "^2.1.1", + "ioredis": "^5.6.1", + "lucide-react": "^0.533.0", "next": "15.4.4", "next-intl": "^4.3.4", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "zod": "^4.0.10" }, "devDependencies": { - "@tailwindcss/postcss": "^4", + "@tailwindcss/postcss": "^4.1.11", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "tailwindcss": "^4", + "daisyui": "^5.0.50", + "tailwindcss": "^4.1.11", "typescript": "^5" } }, @@ -538,6 +542,12 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@ioredis/commands": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", + "integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==", + "license": "MIT" + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1090,6 +1100,15 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -1142,12 +1161,48 @@ "dev": true, "license": "MIT" }, + "node_modules/daisyui": { + "version": "5.0.50", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.50.tgz", + "integrity": "sha512-c1PweK5RI1C76q58FKvbS4jzgyNJSP6CGTQ+KkZYzADdJoERnOxFoeLfDHmQgxLpjEzlYhFMXCeodQNLCC9bow==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -1191,6 +1246,30 @@ "tslib": "^2.8.0" } }, + "node_modules/ioredis": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", + "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -1447,6 +1526,27 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lucide-react": { + "version": "0.533.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.533.0.tgz", + "integrity": "sha512-XwRo6CQowPRe1cfBJITmHytjR3XS4AZpV6rrBFEzoghARgyU2RK3yNRSnRkSFFSQJWFdQ8f4Wk1awgLLSy1NCQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -1496,6 +1596,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1686,6 +1792,27 @@ "react": "^19.1.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -1767,6 +1894,12 @@ "node": ">=0.10.0" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -1875,6 +2008,15 @@ "engines": { "node": ">=18" } + }, + "node_modules/zod": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.10.tgz", + "integrity": "sha512-3vB+UU3/VmLL2lvwcY/4RV2i9z/YU0DTV/tDuYjrwmx5WeJ7hwy+rGEEx8glHp6Yxw7ibRbKSaIFBgReRPe5KA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/ServicesFrontEnd/frontend/package.json b/ServicesFrontEnd/frontend/package.json index 02bfdbc..ed5bda8 100644 --- a/ServicesFrontEnd/frontend/package.json +++ b/ServicesFrontEnd/frontend/package.json @@ -10,17 +10,21 @@ }, "dependencies": { "clsx": "^2.1.1", + "ioredis": "^5.6.1", + "lucide-react": "^0.533.0", "next": "15.4.4", "next-intl": "^4.3.4", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "zod": "^4.0.10" }, "devDependencies": { - "@tailwindcss/postcss": "^4", + "@tailwindcss/postcss": "^4.1.11", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "tailwindcss": "^4", + "daisyui": "^5.0.50", + "tailwindcss": "^4.1.11", "typescript": "^5" } } diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/LoginPage.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/LoginPage.tsx new file mode 100644 index 0000000..77b9a9a --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/LoginPage.tsx @@ -0,0 +1,246 @@ +'use client'; +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)]; +}; + +export default function LoginPage() { + const t = useTranslations('Login'); + const [formData, setFormData] = useState({ + email: '', + password: '' + }); + 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) => { + 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); + } + } + }; + + return ( +
+
+ {/* Recent logins accordion */} + {/*
+
+
setIsAccordionOpen(!isAccordionOpen)} + > +
+
+
+ +
+
+
+

{t('recentLogins')}

+
+
+ + + +
+ + {isAccordionOpen && ( +
+

{t('clickPictureOrAdd')}

+
+
+
+ + +
+ + {t('addAccount')} + +
+
+
+ )} +
+
*/} + +
+ {/* Left side - Login form (now takes full width) */} +
+
+
+
+

{t('welcomeBack')}

+

{t('continueJourney')}

+
+ +
+ +
+ + +
+ {errors.email && ( +
+ + + + + {errors.email} + +
+ )} +
+ +
+ +
+ + + +
+ {errors.password && ( +
+ + + + + {errors.password} + +
+ )} +
+ +
+ + + {t('forgotPassword')} + +
+ + + +
{t('orContinueWith')}
+ +
+ + + +
+ + +
+
+
+
+ +
+

{t('copyright')}

+
+
+
+ ); +} diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/fromFigma.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/fromFigma.tsx new file mode 100644 index 0000000..3182315 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/fromFigma.tsx @@ -0,0 +1,77 @@ +"use client"; + +export default function FromFigma() { + return ( +
+
+
+ {/* Left: Recent logins */} +
+
+

Recent logins

+
Click your picture or add an account
+
+ {/* User card */} +
+
+ Mika Lee + +
+
Mika Lee
+
+ {/* Add account card */} +
+
+ +
+
Add an account
+
+
+
+ {/* Right: Login form */} +
+
+
+
+ + +
+
+
+ +
+ + Hide +
+
+ +
+ + +
+
+ +
+
+
+ {/* Footer */} + +
+ ); +} \ No newline at end of file diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/page.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/page.tsx new file mode 100644 index 0000000..a13a157 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(auth)/login/page.tsx @@ -0,0 +1,21 @@ +'use server'; +import LocaleSwitcherServer from '@/components/LocaleSwitcherServer'; +import LoginPage from './LoginPage'; +import FromFigma from './fromFigma'; + +type Props = { + params: Promise<{ locale: string }>; +}; + +export default async function PageLogin({ params }: Props) { + const { locale } = await params; + return ( +
+
+ +
+ + {/* */} +
+ ); +} \ No newline at end of file diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/layout.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/layout.tsx index 53ddf8f..fedfbbc 100644 --- a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/layout.tsx +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/layout.tsx @@ -1,5 +1,33 @@ import { ReactNode } from 'react'; +import { headers } from 'next/headers'; +import { removeLocaleFromPath } from '@/lib/helpers'; -export default function ProtectedLayout({ children }: { children: ReactNode }) { +function removeSubStringFromPath(headersList: Headers) { + const host = headersList.get('host') || ''; + const referer = headersList.get('referer') || ''; + let currentRoute = ''; + if (referer) { + const hostPart = `http://${host}`; + if (referer.startsWith(hostPart)) { + currentRoute = referer.substring(hostPart.length); + } else { + const secureHostPart = `https://${host}`; + if (referer.startsWith(secureHostPart)) { + currentRoute = referer.substring(secureHostPart.length); + } + } + } + return removeLocaleFromPath(currentRoute); +} + +export default async function ProtectedLayout({ + children, +}: { + children: ReactNode, +}) { + // Server component approach to get URL + const headersList = await headers(); + const removedLocaleRoute = removeSubStringFromPath(headersList); + console.log('Removed locale route:', removedLocaleRoute); return <>{children}; } diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/office/trial/page.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/office/trial/page.tsx new file mode 100644 index 0000000..a7c5adb --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/office/trial/page.tsx @@ -0,0 +1,3 @@ +export default function TrialPage() { + return
TrialPage
; +} \ No newline at end of file diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/venue/trial/page.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/venue/trial/page.tsx new file mode 100644 index 0000000..937bba1 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(protected)/venue/trial/page.tsx @@ -0,0 +1,279 @@ +'use client'; + +import { z } from 'zod'; +import { usePathname } from "next/navigation"; +import { castTextToTypeGiven, removeLocaleFromPath } from "@/lib/helpers"; +import { useState } from 'react'; +import { buildCacheKey } from "@/lib/helpers"; +import { apiPostFetcher } from '@/lib/fetcher'; + +const createFormSchema = z.object({ + inputSelectName: z.string().min(1, "Name is required"), + inputSelectNumber: z.string().refine((val: string) => !isNaN(Number(val)), { + message: "Number must be a valid number", + }), +}); + +const selectFormSchema = z.object({ + inputSelectName: z.string().min(1, "Name is required"), + inputSelectNumber: z.string().refine((val: string) => !isNaN(Number(val)), { + message: "Number must be a valid number", + }), +}); + +const searchFormSchema = z.object({ + inputSearchName: z.string().min(1, "Name is required"), + inputSearchNumber: z.string().refine((val: string) => !isNaN(Number(val)), { + message: "Number must be a valid number", + }), +}); + +type SelectFormData = z.infer; +type CreateFormData = z.infer; +type SearchFormData = z.infer; + + +export default function TrialPage() { + const pathname = usePathname(); + const cleanPathname = removeLocaleFromPath(pathname); + + const cacheKeyCreateForm = buildCacheKey({ url: cleanPathname, form: 'trialCreateForm', field: 'trialCreateField' }); + const cacheKeySelectForm = buildCacheKey({ url: cleanPathname, form: 'trialSelectForm', field: 'trialSelectField' }); + const cacheKeySearchForm = buildCacheKey({ url: cleanPathname, form: 'trialSearchForm', field: 'trialSearchField' }); + + const [createFormErrors, setCreateFormErrors] = useState>({}); + const [selectFormErrors, setSelectFormErrors] = useState>({}); + const [searchFormErrors, setSearchFormErrors] = useState>({}); + + const submitCreateForm = async (e: React.FormEvent) => { + e.preventDefault(); + const form = e.currentTarget; + const formData = new FormData(form); + const formDataObj: Record = {}; + formData.forEach((value, key) => { + formDataObj[key] = value.toString(); + }); + const result: z.ZodSafeParseResult = createFormSchema.safeParse(formDataObj); + if (result.success) { + console.log('Form validation succeeded:', result.data); + setCreateFormErrors({}); + try { + await apiPostFetcher({ + url: 'http://localhost:3000/api/cache/delete', + isNoCache: true, + body: { key: cacheKeyCreateForm } + }); + console.log('Form data saved to Redis'); + } catch (error) { + console.error('Error saving form data:', error); + setCreateFormErrors({ + error: "Error saving form data" + }); + } + } else { + const errors: Record = {}; + result.error.issues.forEach((error: any) => { + const path = error.path.join('.'); + errors[path] = error.message; + }); + console.log('Form validation failed:', errors); + setCreateFormErrors(errors); + } + }; + + const submitSelectForm = async (e: React.FormEvent) => { + e.preventDefault(); + const form = e.currentTarget; + const formName = form.getAttribute('name'); + console.log('Form name:', formName); + const formData = new FormData(form); + const formDataObj: Record = {}; + formData.forEach((value, key) => { + formDataObj[key] = value.toString(); + }); + const result: z.ZodSafeParseResult = selectFormSchema.safeParse(formDataObj); + if (result.success) { + console.log('Form validation succeeded:', result.data); + setSelectFormErrors({}); + + try { + await apiPostFetcher({ + url: 'http://localhost:3000/api/cache/delete', + isNoCache: true, + body: { key: cacheKeySelectForm } + }); + console.log('Form data saved to Redis'); + } catch (error) { + console.error('Error saving form data:', error); + } + } else { + const errors: Record = {}; + result.error.issues.forEach((error: any) => { + const path = error.path.join('.'); + errors[path] = error.message; + }); + console.log('Form validation failed:', errors); + setSelectFormErrors(errors); + } + }; + + const submitSearchForm = async (e: React.FormEvent) => { + e.preventDefault(); + const form = e.currentTarget; + const formName = form.getAttribute('name'); + console.log('Form name:', formName); + const formData = new FormData(form); + const formDataObj: Record = {}; + formData.forEach((value, key) => { formDataObj[key] = value.toString() }); + const result: z.ZodSafeParseResult = searchFormSchema.safeParse(formDataObj); + if (result.success) { + console.log('Form validation succeeded:', result.data); + setSearchFormErrors({}); + try { + await apiPostFetcher({ + url: 'http://localhost:3000/api/cache/delete', + isNoCache: true, + body: { key: cacheKeySearchForm } + }); + console.log('Form data saved to Redis'); + } catch (error) { + console.error('Error saving form data:', error); + } + } else { + const errors: Record = {}; + result.error.issues.forEach((error: any) => { + const path = error.path.join('.'); + errors[path] = error.message; + }); + console.log('Form validation failed:', errors); + setSearchFormErrors(errors); + } + }; + + const handleOnBlurSelectForm = (e: React.FocusEvent) => { + const name = e.target.getAttribute('name'); + const fieldType = e.target.getAttribute('type')?.toString() + const value = e.target.value; + if (!value) { + return; + } + const castedValue = castTextToTypeGiven(value, fieldType as string); + apiPostFetcher( + { + url: 'http://localhost:3000/api/cache/renew', + isNoCache: true, + body: { key: cacheKeySelectForm, value: castedValue, field: name } + } + ).then((res) => { + console.log('Select form onBlur Response', res); + }).catch((err) => { + console.log('Select form onBlur Error', err); + }); + }; + + const handleOnBlurSearchForm = (e: React.FocusEvent) => { + const name = e.target.getAttribute('name'); + const fieldType = e.target.getAttribute('type')?.toString() + const value = e.target.value; + if (!value) { + return; + } + const castedValue = castTextToTypeGiven(value, fieldType as string); + apiPostFetcher( + { + url: 'http://localhost:3000/api/cache/renew', + isNoCache: true, + body: { key: cacheKeySearchForm, value: castedValue, field: name } + } + ).then((res) => { + console.log('Search form onBlur Response', res); + }).catch((err) => { + console.log('Search form onBlur Error', err); + }); + }; + + const handleOnBlurCreateForm = (e: React.FocusEvent) => { + const name = e.target.getAttribute('name'); + const fieldType = e.target.getAttribute('type')?.toString() + const value = e.target.value; + if (!value) { + return; + } + const castedValue = castTextToTypeGiven(value, fieldType as string); + apiPostFetcher( + { + url: 'http://localhost:3000/api/cache/renew', + isNoCache: true, + body: { key: cacheKeyCreateForm, value: castedValue, field: name } + } + ).then((res) => { + console.log('Create form onBlur Response', res); + }).catch((err) => { + console.log('Create form onBlur Error', err); + }); + }; + + return
+
+

Form Keys

+
+
+
+
CreateForm: {cacheKeyCreateForm}
+
SelectForm: {cacheKeySelectForm}
+
SearchForm: {cacheKeySearchForm}
+
+
+ + {/* Create a simple Form */} +
+
+ + {createFormErrors.inputSelectName &&

{createFormErrors.inputSelectName}

} + + {createFormErrors.inputSelectNumber &&

{createFormErrors.inputSelectNumber}

} + +
+ {/* Select a simple Form */} +
+ + {selectFormErrors.inputSelectName &&

{selectFormErrors.inputSelectName}

} + + {selectFormErrors.inputSelectNumber &&

{selectFormErrors.inputSelectNumber}

} + +
+ {/* Search a simple Form */} +
+ + {searchFormErrors.inputSearchName &&

{searchFormErrors.inputSearchName}

} + + {searchFormErrors.inputSearchNumber &&

{searchFormErrors.inputSearchNumber}

} + +
+
+
; +} \ No newline at end of file diff --git a/ServicesFrontEnd/frontend/src/app/[locale]/(public)/home/page.tsx b/ServicesFrontEnd/frontend/src/app/[locale]/(public)/home/page.tsx index fc5fd4a..89776be 100644 --- a/ServicesFrontEnd/frontend/src/app/[locale]/(public)/home/page.tsx +++ b/ServicesFrontEnd/frontend/src/app/[locale]/(public)/home/page.tsx @@ -1,19 +1,14 @@ -// Server Component +import LocaleSwitcherServer from '@/components/LocaleSwitcherServer'; import { getTranslations } from 'next-intl/server'; import { Link } from '@/i18n/navigation'; import { Locale } from '@/i18n/locales'; -import LocaleSwitcherServer from '@/components/LocaleSwitcherServer'; -// Define the props type to get the locale parameter type Props = { params: Promise<{ locale: string }>; }; export default async function HomePage({ params }: Props) { - // Get the locale from params const { locale } = await params; - - // Get translations with the correct locale const t = await getTranslations({ locale: locale as Locale, namespace: 'Index' }); return ( diff --git a/ServicesFrontEnd/frontend/src/app/api/a.txt b/ServicesFrontEnd/frontend/src/app/api/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/delete/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/delete/route.ts new file mode 100644 index 0000000..72aaddc --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/cache/delete/route.ts @@ -0,0 +1,8 @@ +import { NextRequest, NextResponse } from "next/server"; +import { deleteKey } from "@/libss/redisService"; + +export async function POST(req: NextRequest) { + const { key } = await req.json(); + await deleteKey({ key }); + return NextResponse.json({ status: "ok", message: `Deleted "${key}"` }); +} diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/exists/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/exists/route.ts new file mode 100644 index 0000000..38b7ff4 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/cache/exists/route.ts @@ -0,0 +1,8 @@ +import { NextRequest, NextResponse } from "next/server"; +import { exists } from "@/libss/redisService"; + +export async function POST(req: NextRequest) { + const { key } = await req.json(); + const result = await exists({ key }); + return NextResponse.json({ status: "ok", exists: result }); +} diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/get/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/get/route.ts new file mode 100644 index 0000000..4d7dcad --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/cache/get/route.ts @@ -0,0 +1,11 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getJSON } from "@/libss/redisService"; + +export async function POST(req: NextRequest) { + const { key } = await req.json(); + const result = await getJSON({ key }); + + return result + ? NextResponse.json({ status: "ok", data: result }) + : NextResponse.json({ status: "not_found" }, { status: 404 }); +} diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/renew/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/renew/route.ts new file mode 100644 index 0000000..f61c16b --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/cache/renew/route.ts @@ -0,0 +1,12 @@ +import { NextRequest, NextResponse } from "next/server"; +import { updateField } from "@/libss/redisService"; + +export async function POST(req: NextRequest) { + try { + const { key, field, value } = await req.json(); + await updateField({ key, field, value }); + return NextResponse.json({ status: "ok", message: `Updated "${key}"` }); + } catch (err: any) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/route.ts new file mode 100644 index 0000000..a5390a4 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/cache/route.ts @@ -0,0 +1,45 @@ +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 new file mode 100644 index 0000000..eca69bf --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/cache/set/route.ts @@ -0,0 +1,8 @@ +import { NextRequest, NextResponse } from "next/server"; +import { setJSON } from "@/libss/redisService"; + +export async function POST(req: NextRequest) { + const { key, value, ttlSeconds } = await req.json(); + await setJSON({ key, value, ttlSeconds }); + return NextResponse.json({ status: "ok", message: `Set "${key}"` }); +} diff --git a/ServicesFrontEnd/frontend/src/app/api/cache/update/route.ts b/ServicesFrontEnd/frontend/src/app/api/cache/update/route.ts new file mode 100644 index 0000000..eb25214 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/app/api/cache/update/route.ts @@ -0,0 +1,12 @@ +import { NextRequest, NextResponse } from "next/server"; +import { updateJSON } from "@/libss/redisService"; + +export async function POST(req: NextRequest) { + try { + const { key, value } = await req.json(); + await updateJSON({ key, value }); + return NextResponse.json({ status: "ok", message: `Updated "${key}"` }); + } catch (err: any) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/ServicesFrontEnd/frontend/src/app/globals.css b/ServicesFrontEnd/frontend/src/app/globals.css index a2dc41e..fa4f694 100644 --- a/ServicesFrontEnd/frontend/src/app/globals.css +++ b/ServicesFrontEnd/frontend/src/app/globals.css @@ -1,4 +1,10 @@ @import "tailwindcss"; +@import "tailwindcss"; +@plugin "daisyui"; + +/* @plugin "daisyui" { + themes: all; +} */ :root { --background: #ffffff; diff --git a/ServicesFrontEnd/frontend/src/i18n/en.json b/ServicesFrontEnd/frontend/src/i18n/en.json index 759c293..7525bfb 100644 --- a/ServicesFrontEnd/frontend/src/i18n/en.json +++ b/ServicesFrontEnd/frontend/src/i18n/en.json @@ -17,5 +17,30 @@ }, "LocaleLayout": { "title": "My Application" + }, + "Login": { + "title": "Login", + "description": "Login to your account", + "email": "Your email", + "password": "Your password", + "login": "Log in", + "register": "Register", + "createAccount": "Create an account", + "forgotPassword": "Forgot your password?", + "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" } } diff --git a/ServicesFrontEnd/frontend/src/i18n/tr.json b/ServicesFrontEnd/frontend/src/i18n/tr.json index 4c0aa9a..2e63bcf 100644 --- a/ServicesFrontEnd/frontend/src/i18n/tr.json +++ b/ServicesFrontEnd/frontend/src/i18n/tr.json @@ -17,5 +17,30 @@ }, "LocaleLayout": { "title": "Uygulamam" + }, + "Login": { + "title": "Giriş Yap", + "description": "Hesabınıza giriş yap", + "email": "E-posta adresiniz", + "password": "Parolanız", + "login": "Giriş yap", + "register": "Kayıt Ol", + "createAccount": "Hesap oluştur", + "forgotPassword": "Parolanızı mı unuttunuz?", + "recentLogins": "Son girişler", + "clickPictureOrAdd": "Resminize tıklayın veya 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 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": "Yolculuğunuza devam etmek için giriş yapın" } } diff --git a/ServicesFrontEnd/frontend/src/lib/fetcher.tsx b/ServicesFrontEnd/frontend/src/lib/fetcher.tsx new file mode 100644 index 0000000..016e285 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/lib/fetcher.tsx @@ -0,0 +1,140 @@ +interface FetcherRequest { + url: string; + isNoCache: boolean; +} + +interface PostFetcherRequest extends FetcherRequest { + body: Record; +} + +interface GetFetcherRequest extends FetcherRequest { + url: string; +} + +interface DeleteFetcherRequest extends GetFetcherRequest { } +interface PutFetcherRequest extends PostFetcherRequest { } +interface PatchFetcherRequest extends PostFetcherRequest { } + +interface FetcherRespose { + success: boolean; +} +interface PaginationResponse { + onPage: number; + onPageCount: number; + totalPage: number; + totalCount: number; + next: boolean; + back: boolean; +} + +interface FetcherDataResponse extends FetcherRespose { + data: Record | null; + pagination?: PaginationResponse; +} + +async function apiPostFetcher({ + url, + isNoCache, + body, +}: PostFetcherRequest): Promise> { + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" }, + body: JSON.stringify(body), + }) + if (response.status === 200) { + const result = await response.json(); + return { success: true, data: result.data, pagination: result.pagination } + } + } catch (error) { } + return { success: false, data: null } +} + + +async function apiGetFetcher({ + url, + isNoCache, +}: GetFetcherRequest): Promise> { + try { + const response = await fetch(url, { + method: "GET", + headers: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" }, + }) + console.log("apiGetFetcher status", await response.text()); + if (response.status === 200) { + const result = await response.json(); + return { success: true, data: result.data } + } + } catch (error) { } + return { success: false, data: null } +} + + +async function apiDeleteFetcher({ + url, + isNoCache, +}: DeleteFetcherRequest): Promise> { + try { + const response = await fetch(url, { + method: "DELETE", + headers: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" }, + }) + if (response.status === 200) { + const result = await response.json(); + return { + success: true, + data: result.data, + } + } + } catch (error) { } + return { success: false, data: null } +} + + +async function apiPutFetcher({ + url, + isNoCache, + body, +}: PutFetcherRequest): Promise> { + try { + const response = await fetch(url, { + method: "PUT", + headers: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" }, + body: JSON.stringify(body), + }) + if (response.status === 200) { + const result = await response.json(); + return { + success: true, + data: result.data, + } + } + } catch (error) { } + return { success: false, data: null } +} + + +async function apiPatchFetcher({ + url, + isNoCache, + body, +}: PatchFetcherRequest): Promise> { + try { + const response = await fetch(url, { + method: "PATCH", + headers: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" }, + body: JSON.stringify(body), + }) + if (response.status === 200) { + const result = await response.json(); + return { + success: true, + data: result.data, + } + } + } catch (error) { } + return { success: false, data: null } +} + +export { apiPostFetcher, apiGetFetcher, apiDeleteFetcher, apiPutFetcher, apiPatchFetcher }; diff --git a/ServicesFrontEnd/frontend/src/lib/helpers.tsx b/ServicesFrontEnd/frontend/src/lib/helpers.tsx new file mode 100644 index 0000000..8d72878 --- /dev/null +++ b/ServicesFrontEnd/frontend/src/lib/helpers.tsx @@ -0,0 +1,30 @@ +import crypto from 'crypto'; + +export const buildCacheKey = ({ url, form, field }: { url: string, form: string, field: string }) => { + const raw = `${url}::${form}::${field}`; + const hash = crypto.createHash('sha1').update(raw).digest('hex'); + return `${hash}`; +}; + +export function removeLocaleFromPath(path: string) { + return '/' + path.split('/').slice(2).join('/'); +} + + +export function castTextToTypeGiven(value: string, type: string) { + try { + switch (type) { + case 'decimal': + return parseFloat(value); + case 'number': + return Number(value); + case 'boolean': + return Boolean(value); + default: + return value; + } + } catch (error) { + console.log(error); + return value; + } +} \ No newline at end of file diff --git a/ServicesFrontEnd/frontend/src/libss/redis.ts b/ServicesFrontEnd/frontend/src/libss/redis.ts new file mode 100644 index 0000000..178087f --- /dev/null +++ b/ServicesFrontEnd/frontend/src/libss/redis.ts @@ -0,0 +1,17 @@ +import Redis from "ioredis"; + +const redis = new Redis({ + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT || "6379", 10), + password: process.env.REDIS_PASSWORD || "", + db: parseInt(process.env.REDIS_DB || "0", 10), + connectTimeout: 5000, + maxRetriesPerRequest: 2, + retryStrategy: (times) => Math.min(times * 50, 2000), + reconnectOnError: (err) => err.message.includes("READONLY"), +}); + +redis.on("connect", () => console.log("[redis] Connected")); +redis.on("error", (err) => console.error("[redis] Error:", err)); + +export default redis; diff --git a/ServicesFrontEnd/frontend/src/libss/redisService.ts b/ServicesFrontEnd/frontend/src/libss/redisService.ts new file mode 100644 index 0000000..830befe --- /dev/null +++ b/ServicesFrontEnd/frontend/src/libss/redisService.ts @@ -0,0 +1,102 @@ +import redis from "./redis"; + +interface SScanParams { + rKey: string; +} + +interface DScanParams { + rKey: string; + sKey: string; +} + +interface SetParams { + key: string; + value: T; + ttlSeconds?: number; +} + +interface GetParams { + key: string; +} + +interface UpdateParams { + key: string; + value: T; +} + +interface UpdateFieldParams { + key: string; + field: string; + value: T; +} + +type RScan = Promise; +type RExists = Promise; +type RUpdate = Promise; +type RDelete = Promise; +type RSet = Promise; +type RJSON = Promise; + +export async function setJSON(params: SetParams): RSet { + const val = JSON.stringify(params.value); + if (params.ttlSeconds) { + await redis.set(params.key, val, "EX", params.ttlSeconds); + } else { + await redis.set(params.key, val); + } +} + +export async function getJSON(params: GetParams): RJSON { + const val = await redis.get(params.key); + if (!val) return null; + try { + return JSON.parse(val); + } catch { + return null; + } +} + +export async function exists(params: GetParams): RExists { + const result = await redis.exists(params.key); + return result === 1; +} + +export async function updateJSON(params: UpdateParams): RUpdate { + const keyExists = await redis.exists(params.key); + if (!keyExists) throw new Error("Key not found"); + await redis.set(params.key, JSON.stringify(params.value)); +} + +export async function updateField(params: UpdateFieldParams): RExists { + const isExists = await exists({ key: params.key }); + if (!isExists) { + await setJSON({ + key: params.key, + value: { [params.field as string]: params.value }, + }); + return true; + } + const valueFromRedis = await getJSON({ key: params.key }); + if (!valueFromRedis) throw new Error("Key not found"); + await updateJSON({ + key: params.key, + value: { ...valueFromRedis, [params.field as string]: params.value }, + }); + return true; +} + +export async function deleteKey(params: GetParams): RDelete { + await redis.del(params.key); +} + +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; +} + +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; +} diff --git a/proxy-md.md b/proxy-md.md new file mode 100644 index 0000000..237e928 --- /dev/null +++ b/proxy-md.md @@ -0,0 +1,129 @@ +```docker-compose.yml +version: '3.8' + +services: + nginx: + image: owasp/modsecurity:nginx + container_name: secure_nginx + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/html:/usr/share/nginx/html:ro + - ./nginx/modsecurity/modsecurity.conf:/etc/modsecurity/modsecurity.conf:ro + - ./nginx/log:/var/log/nginx + restart: always + + fail2ban: + image: crazymax/fail2ban:latest + container_name: fail2ban + volumes: + - ./fail2ban/jail.local:/data/jail.local:ro + - ./fail2ban/filter.d:/data/filter.d:ro + - ./nginx/log:/var/log/nginx:ro + - fail2ban-data:/data + restart: always + cap_add: + - NET_ADMIN + - NET_RAW + +volumes: + fail2ban-data: +``` + +```conf +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + modsecurity on; + modsecurity_rules_file /etc/modsecurity/modsecurity.conf; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + limit_req_zone $binary_remote_addr zone=limit1:10m rate=5r/s; + + server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + + location / { + limit_req zone=limit1 burst=10 nodelay; + try_files $uri $uri/ =404; + } + } +} +``` + +nginx/html/index.html + +``` + + + + ModSecurity + Fail2Ban + + +

Güvenlik Duvarı Aktif!

+ + +``` + +nginx/modsecurity/modsecurity.conf + +``` +SecRuleEngine On +SecRequestBodyAccess On +SecResponseBodyAccess Off + +SecRule REQUEST_HEADERS:User-Agent "curl" "id:10001,phase:1,deny,status:403,msg:'Curl client blocked'" + +# Basit rate limit sayacı +SecAction "id:10010,phase:1,initcol:ip=%{REMOTE_ADDR},pass,nolog" +SecRule IP:REQCOUNT "@gt 20" "id:10011,phase:2,deny,status:429,msg:'Too many requests'" +SecAction "id:10012,phase:2,pass,nolog,setvar:ip.reqcount=+1" +``` + +fail2ban/jail.local + +``` +[nginx-req-limit] +enabled = true +filter = nginx-req-limit +action = iptables[name=HTTP, port=http, protocol=tcp] +logpath = /var/log/nginx/access.log +maxretry = 5 +findtime = 60 +bantime = 3600 +``` + +fail2ban/filter.d/nginx-req-limit.conf + +``` +[Definition] +failregex = -.* "(GET|POST).*HTTP.*" 429 +ignoreregex = + +``` + +mkdir -p nginx/html nginx/modsecurity fail2ban/filter.d +docker-compose up -d