Compare commits

...

27 Commits

Author SHA1 Message Date
9dd8740171 redis handler implemented Servies Task has redis object 2025-08-20 22:25:24 +03:00
4e6774a15b updated Services Task database 2025-08-19 20:17:14 +03:00
e4f6afbc93 parser excel publish chain task build 2025-08-18 18:31:29 +03:00
61529f7d94 rabbitmq implemented and tested 2025-08-17 21:14:46 +03:00
9543d136aa task services added 2025-08-15 22:30:21 +03:00
456203f5cf Comment Parser Regex service completed 2025-08-14 21:20:40 +03:00
82b1d4825b updated Parser comment 2025-08-14 00:11:38 +03:00
4ec9031005 updated Parser comment 2025-08-14 00:10:57 +03:00
7a5521648c Comment service added mail sender service 2025-08-11 20:22:26 +03:00
ca98adc338 Service Runner Finder and complete task chain completed 2025-08-11 19:26:49 +03:00
405ba2e95d ignore added 2025-08-10 11:36:07 +03:00
7452e05a92 venv and prisma cahce deleted 2025-08-10 11:30:56 +03:00
bd12fe02ae updated async prisma runner 2025-08-10 11:29:09 +03:00
a00c2942f5 async prisma runner completed 2025-08-10 11:23:18 +03:00
768f0a5daf updated prisma service async runner 2025-08-10 11:18:00 +03:00
c2fd263f27 Revert "updated Service Runner email Reader"
This reverts commit 81184a8acc.
2025-08-10 11:14:54 +03:00
ac1980566a Revert "updated prisma service async runner"
This reverts commit db0ae34948.
2025-08-10 11:05:45 +03:00
db0ae34948 updated prisma service async runner 2025-08-10 11:01:26 +03:00
81184a8acc updated Service Runner email Reader 2025-08-08 17:15:19 +03:00
a830cc079d page retriever and menu @redis added 2025-08-07 13:04:02 +03:00
a986ddbb95 added pages tested via backend 2025-08-06 16:33:03 +03:00
9232da69d3 updated event controllers and service event mtach tested 2025-08-05 14:42:24 +03:00
aa8f0b8f31 updated accounts service navigator 2025-08-03 18:23:26 +03:00
1b87dee60d updated events 2025-08-02 20:08:43 +03:00
b54bbe2db2 updated select response 2025-07-31 22:37:40 +03:00
924b538559 updated frontend and auth backend service 2025-07-31 17:20:49 +03:00
0ce522d04a updated login page with qwen 2025-07-29 23:06:15 +03:00
248 changed files with 22567 additions and 788 deletions

8
.gitignore vendored
View File

@@ -54,3 +54,11 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
env
.env
**/.prisma-cache
venv/
.vscode/
__pycache__/
*.py[cod]

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"git.ignoreLimitWarning": true
}

0
=9.4.1 Normal file
View File

View File

@@ -48,3 +48,15 @@ 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
npm install next-crypto

View File

@@ -20,6 +20,9 @@ ENV NODE_ENV=production
COPY --from=builder /usr/src/app/dist ./dist
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/package.json ./package.json
RUN npm prune --production
USER node

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"ioredis": "^5.6.1",
"mongodb": "^6.18.0",
"otplib": "^12.0.1",
"qrcode": "^1.5.4",
"redis": "^5.6.1",
@@ -46,7 +47,7 @@
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.16.5",
"@types/node": "^22.17.0",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",

View File

@@ -403,6 +403,7 @@ model account_records {
accounting_receipt_number Int @default(0)
status_id Int @default(0) @db.SmallInt
approved_record Boolean @default(false)
is_predicted Boolean @default(false)
import_file_name String? @db.VarChar
receive_debit Int?
receive_debit_uu_id String? @db.VarChar
@@ -1932,6 +1933,20 @@ model build_ibans {
@@index([updated_at], map: "ix_build_ibans_updated_at")
}
model user_types {
id Int @id @default(autoincrement())
uu_id String @unique(map: "ix_user_types_uu_id") @default(dbgenerated("gen_random_uuid()")) @db.Uuid
type String @db.VarChar
description String @default("") @db.VarChar
type_token String @default("") @db.VarChar
token String @default("") @db.VarChar
occupant_types occupant_types[]
staff staff[]
@@index([type], map: "ix_user_types_type")
@@index([token], map: "ix_user_types_token")
}
/// 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 build_living_space {
fix_value Decimal @default(0) @db.Decimal(20, 6)
@@ -3080,7 +3095,9 @@ model occupant_types {
occupant_code String @default("") @db.VarChar
occupant_category String @default("") @db.VarChar
occupant_category_type String @default("") @db.VarChar
function_retriever String @default("") @db.VarChar
// function_retriever String @default("") @db.VarChar
user_type_id Int?
user_type_uu_id String? @db.VarChar
occupant_is_unique Boolean @default(false)
ref_id String? @db.VarChar(100)
replication_id Int @default(0) @db.SmallInt
@@ -3103,6 +3120,7 @@ model occupant_types {
build_decision_book_person_occupants build_decision_book_person_occupants[]
build_living_space build_living_space[]
build_management build_management[]
user_types user_types? @relation(fields: [user_type_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
@@index([created_at], map: "ix_occupant_types_created_at")
@@index([cryp_uu_id], map: "ix_occupant_types_cryp_uu_id")
@@ -3475,7 +3493,9 @@ model staff {
staff_code String @db.VarChar
duties_id Int
duties_uu_id String @db.VarChar
function_retriever String @default("") @db.VarChar
// function_retriever String @default("") @db.VarChar
user_type_id Int?
user_type_uu_id String? @db.VarChar
ref_id String? @db.VarChar(100)
replication_id Int @default(0) @db.SmallInt
cryp_uu_id String? @db.VarChar
@@ -3497,6 +3517,7 @@ model staff {
employee_history employee_history[]
employees employees[]
duties duties @relation(fields: [duties_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
user_types user_types? @relation(fields: [user_type_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
@@index([created_at], map: "ix_staff_created_at")
@@index([cryp_uu_id], map: "ix_staff_cryp_uu_id")
@@ -3508,6 +3529,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

View File

@@ -8,35 +8,34 @@ import {
Body,
HttpCode,
UseGuards,
NotFoundException,
Req,
Query,
} from '@nestjs/common';
import { AccountsService } from './accounts.service';
import { AuthControlGuard, EndpointControlGuard } from '../middleware/access-control.guard';
import { AuthControlGuard, EndpointControlGuard } from '@/src/middleware/access-control.guard';
import { Navigator } from '@/src/utils/navigator/navigator';
@Controller('accounts')
export class AccountsController {
constructor(private accountsService: AccountsService) {}
constructor(
private accountsService: AccountsService,
private navigator: Navigator
) { }
@Get('events')
@HttpCode(200)
@UseGuards(AuthControlGuard)
async getEvents(@Query() query: any) {
const { userToken } = query;
if (!userToken) { throw new NotFoundException('User token is missing or null') }
const events = await this.navigator.getInfos(this.accountsService, userToken)
return { events, message: "Events fetched successfully" };
}
@Post('filter')
@HttpCode(200)
@UseGuards(AuthControlGuard, EndpointControlGuard)
async filterAccounts(@Body() query: any) {
const result = await this.accountsService.findWithPagination(query);
const { pagination, data } = result;
if (data.length === 0) {
return { pagination, data: [] };
}
const resultRefined = data.map((rec: any) => ({
...rec,
build_decision_book_payments: rec.build_decision_book_payments?.map(
(pmt: any) => ({
...pmt,
ratePercent:
((pmt.payment_amount / rec.currency_value) * 100).toFixed(2) + '%',
}),
),
}));
return { pagination, data: resultRefined };
}
async filterAccounts(@Body() query: any, @Req() req: any) { return await this.navigator.getFunction(req, this.accountsService.mapper, query) }
}

View File

@@ -2,21 +2,31 @@ import { Module } from '@nestjs/common';
import { AccountsService } from './accounts.service';
import { AccountsController } from './accounts.controller';
import { PrismaModule } from '@/prisma/prisma.module';
import { CacheService } from '../cache.service';
import { CacheService } from '../database/redis/redis.service';
import { UtilsModule } from '../utils/utils.module';
import { RedisModule } from '../database/redis/redis.module';
import {
AuthControlGuard,
EndpointControlGuard,
} from '@/src/middleware/access-control.guard';
import { SuperUsersService } from './superusers/superusers.service';
import { UrlHandler } from '../utils/navigator/urlHandler';
import { Navigator } from '@/src/utils/navigator/navigator';
import { NavigatorModule } from '../navigator/navigator.module';
@Module({
imports: [PrismaModule, UtilsModule],
imports: [PrismaModule, UtilsModule, RedisModule, NavigatorModule],
providers: [
AccountsService,
CacheService,
AuthControlGuard,
EndpointControlGuard,
SuperUsersService,
UrlHandler,
Navigator,
],
controllers: [AccountsController],
})
export class AccountsModule {}
export class AccountsModule {
constructor() { }
}

View File

@@ -1,42 +1,39 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/src/prisma.service';
import { Prisma, account_records } from '@prisma/client';
import { CacheService } from '../cache.service';
import { PaginationHelper, PaginationInfo } from '../utils/pagination-helper';
import { SuperUsersService } from './superusers/superusers.service';
import { EventsService } from '../navigator/events/events.service';
@Injectable()
export class AccountsService {
mapper: any
constructor(
private prisma: PrismaService,
private cacheService: CacheService,
private paginationHelper: PaginationHelper,
) {}
async findAll(filter: any): Promise<Partial<account_records>[]> {
return this.prisma.account_records.findMany({
where: { ...filter },
});
private superUsersService: SuperUsersService,
private eventService: EventsService,
) {
this.mapper = {
"j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA": this.superUsersService,
}
}
async findDynamic(
query: Prisma.account_recordsFindManyArgs,
): Promise<{ totalCount: number; result: Partial<account_records>[] }> {
const totalCount = await this.prisma.account_records.count({
where: query.where,
});
const result = await this.prisma.account_records.findMany(query);
return { totalCount, result };
}
async onModuleInit() {
Object.entries(this.mapper).map(async ([key, value]) => {
const service = value as any
await this.eventService.setEvents(service.events, "AccountsService")
})
// const accountPages = await fetch(
// "http://localhost:3000/pages",
// {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({
// token: 'j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA',
// pages: {
async findWithPagination(
query: any & { page?: number; pageSize?: number },
): Promise<{ data: any[]; pagination: PaginationInfo }> {
return this.paginationHelper.paginate(this.prisma.account_records, query);
}
async findOne(uuid: string): Promise<Partial<account_records> | null> {
return this.prisma.account_records.findUnique({
where: { uu_id: uuid },
});
// }
// })
// }
// )
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SuperusersService } from './superusers.service';
describe('SuperusersService', () => {
let service: SuperusersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SuperusersService],
}).compile();
service = module.get<SuperusersService>(SuperusersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,54 @@
import { PaginationHelper } from '@/src/utils/pagination-helper';
import { Injectable } from '@nestjs/common';
import { PaginationInfo } from '@/src/utils/pagination-helper';
import { PrismaService } from '@/src/prisma.service';
import { UrlHandler } from '@/src/utils/navigator/urlHandler';
@Injectable()
export class SuperUsersService {
userToken: string = "j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA"
constructor(
private paginationHelper: PaginationHelper,
private prisma: PrismaService,
private urlHandler: UrlHandler,
) { }
events = {
"e6hewIe7YqbQZHO3:j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA": [
{
"key": "qt5P0xoeThjNT9EuWfwBgxsntHY5ydRtKFr1pgKGcgxx",
"endpoint": "/accounts/filter:POST",
"eToken": "e6hewIe7YqbQZHO3",
"token": "j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA",
"description": "Super Users Account Filter",
"isDefault": true,
"query": { "query": true, "page": false, "pageSize": false },
"pages": []
}
]
};
mapper = {
"e6hewIe7YqbQZHO3:j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA:qt5P0xoeThjNT9EuWfwBgxsntHY5ydRtKFr1pgKGcgxx": (query: any) => this.filter(query),
}
async getEvents() { return this.urlHandler.getEvents(this.events, this.mapper) }
async infoEvents(userToken: string) { return Object.entries(this.events).filter(([key]) => key.endsWith(userToken)) }
async filter(query: any & { page?: number; pageSize?: number }): Promise<{ pagination: PaginationInfo; data: any[] }> {
const result = await this.paginationHelper.findWithPagination(query, this.prisma.account_records);
const { pagination, data } = result;
if (data.length === 0) { return { pagination, data: [] } }
const resultRefined = data.map((rec: any) => ({
...rec,
build_decision_book_payments: rec.build_decision_book_payments?.map(
(pmt: any) => ({
...pmt,
ratePercent: ((pmt.payment_amount / rec.currency_value) * 100).toFixed(2) + '%',
}),
),
}));
return { pagination, data: resultRefined };
}
}

View File

@@ -7,34 +7,26 @@ import {
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { PrismaModule } from '@/prisma/prisma.module';
import { PrismaModule } from '../prisma/prisma.module';
import { AccountsModule } from './accounts/accounts.module';
import { AuthModule } from './auth/auth.module';
import { RedisModule } from '@liaoliaots/nestjs-redis';
import { CacheService } from './cache.service';
import { RedisModule } from './database/redis/redis.module';
import { LoggerMiddleware } from '@/src/middleware/logger.middleware';
import { DiscoveryModule } from '@nestjs/core';
const redisConfig = {
host: '10.10.2.15',
port: 6379,
password: 'your_strong_password_here',
};
import { NavigatorModule } from './navigator/navigator.module';
const modulesList = [UsersModule, AccountsModule, AuthModule];
const serviceModuleList = [
PrismaModule,
RedisModule.forRoot({
config: redisConfig,
}),
RedisModule.forRootWithConfig(true),
DiscoveryModule,
];
const controllersList = [AppController];
const providersList = [AppService, CacheService];
const exportsList = [CacheService];
const providersList = [AppService];
const exportsList = [];
@Module({
imports: [...serviceModuleList, ...modulesList],
imports: [...serviceModuleList, ...modulesList, NavigatorModule],
controllers: controllersList,
providers: providersList,
exports: exportsList,

View File

@@ -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')

View File

@@ -11,9 +11,12 @@ import { ResetPasswordService } from './password/reset/reset.service';
import { ChangePasswordService } from './password/change/change.service';
import { VerifyOtpService } from './password/verify-otp/verify-otp.service';
import { DisconnectService } from './disconnect/disconnect.service';
import { MongoModule } from '@/src/database/mongo/mongo.module';
import { MongoService } from '@/src/database/mongo/mongo.service';
import { NavigatorModule } from '../navigator/navigator.module';
@Module({
imports: [UtilsModule],
imports: [UtilsModule, MongoModule, NavigatorModule],
controllers: [AuthController],
providers: [
AuthService,
@@ -25,8 +28,9 @@ import { DisconnectService } from './disconnect/disconnect.service';
ResetPasswordService,
VerifyOtpService,
DisconnectService,
MongoService,
PrismaService,
],
exports: [AuthService],
})
export class AuthModule {}
export class AuthModule { }

View File

@@ -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) {

View File

@@ -1,4 +1,4 @@
import { IsObject, IsOptional, IsString, IsBoolean } from 'class-validator';
import { IsOptional, IsString, IsBoolean } from 'class-validator';
export class userLoginValidator {
@IsString()

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { userLoginValidator } from '@/src/auth/login/dtoValidator';
import { RedisHandlers } from '@/src/utils/auth/redis_handlers';
import { PasswordHandlers } from '@/src/utils/auth/login_handler';
import { RedisHandlers } from '@/src/utils/store/redisHandlers';
import { PasswordHandlers } from '@/src/utils/store/loginHandler';
import { PrismaService } from '@/src/prisma.service';
import { AuthTokenSchema } from '@/src/types/auth/token';
@@ -11,47 +11,157 @@ export class LoginService {
private readonly redis: RedisHandlers,
private readonly passHandlers: PasswordHandlers,
private readonly prisma: PrismaService,
) {}
) { }
async run(dto: userLoginValidator) {
const foundUser = await this.prisma.users.findFirstOrThrow({
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,
dto.password,
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,
credentials: {
person_id: foundPerson.id,
person_name: foundPerson.firstname,
},
});
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;
}
let fullName = `${foundPerson.firstname}`;
if (foundPerson.middle_name) fullName += ` ${foundPerson.middle_name}`;
if (foundPerson.birthname) fullName += ` ${foundPerson.birthname}`;
fullName += ` ${foundPerson.surname}`;
const redisData = AuthTokenSchema.parse({
people: foundPerson,
users: foundUser,
credentials: {
uuid: foundPerson.uu_id,
fullName: fullName,
},
selectionList: {
type: foundUser.user_type,
list: selectList,
},
});
const accessToken = await this.redis.setLoginToRedis(
redisData,
foundUser.uu_id,
);
return {
token: accessToken,
message: 'Login successful',
};
}
}
}

View File

@@ -5,8 +5,8 @@ import {
UnauthorizedException,
} from '@nestjs/common';
import { userChangePasswordValidator } from './dtoValidator';
import { RedisHandlers } from '@/src/utils/auth/redis_handlers';
import { PasswordHandlers } from '@/src/utils/auth/login_handler';
import { RedisHandlers } from '@/src/utils/store/redisHandlers';
import { PasswordHandlers } from '@/src/utils/store/loginHandler';
@Injectable()
export class ChangePasswordService {
@@ -14,7 +14,7 @@ export class ChangePasswordService {
private readonly prisma: PrismaService,
private readonly redis: RedisHandlers,
private readonly passHandlers: PasswordHandlers,
) {}
) { }
private async syncPasswordHistory(
foundUser: any,
@@ -84,18 +84,18 @@ export class ChangePasswordService {
password: hashPassword,
...(oldestIndex === 'first'
? {
old_password_first: dto.password,
old_password_first_modified_at: new Date(),
}
old_password_first: dto.password,
old_password_first_modified_at: new Date(),
}
: oldestIndex === 'second'
? {
old_password_second: dto.password,
old_password_second_modified_at: new Date(),
}
old_password_second: dto.password,
old_password_second_modified_at: new Date(),
}
: {
old_password_third: dto.password,
old_password_third_modified_at: new Date(),
}),
old_password_third: dto.password,
old_password_third_modified_at: new Date(),
}),
},
});
}

View File

@@ -1,6 +1,6 @@
import { userCreatePasswordValidator } from './dtoValidator';
import { PrismaService } from '@/src/prisma.service';
import { PasswordHandlers } from '@/src/utils/auth/login_handler';
import { PasswordHandlers } from '@/src/utils/store/loginHandler';
import { Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
@@ -8,7 +8,7 @@ export class CreatePasswordService {
constructor(
private readonly prisma: PrismaService,
private readonly passHandlers: PasswordHandlers,
) {}
) { }
async run(dto: userCreatePasswordValidator) {
if (dto.password !== dto.rePassword) {

View File

@@ -1,14 +1,14 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { userResetPasswordValidator } from './dtoValidator';
import { PrismaService } from '@/src/prisma.service';
import { PasswordHandlers } from '@/src/utils/auth/login_handler';
import { PasswordHandlers } from '@/src/utils/store/loginHandler';
@Injectable()
export class ResetPasswordService {
constructor(
private readonly prisma: PrismaService,
private readonly passHandlers: PasswordHandlers,
) {}
) { }
async run(dto: userResetPasswordValidator) {
const foundUser = await this.prisma.users.findFirstOrThrow({
where: {

View File

@@ -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 → frontende gönder, <img src="data:image/png;base64,..."> diye gösterilebilir

View File

@@ -2,5 +2,5 @@ import { IsString } from 'class-validator';
export class userSelectValidator {
@IsString()
selected_uu_id: string;
uuid: string;
}

View File

@@ -1,9 +1,268 @@
import { Injectable } from '@nestjs/common';
import { Injectable, UnauthorizedException, NotAcceptableException } from '@nestjs/common';
import { userSelectValidator } from '@/src/auth/select/dtoValidator';
import { RedisHandlers } from '@/src/utils/store/redisHandlers';
import { EmployeeTokenSchema, OccupantTokenSchema, UserType } from '@/src/types/auth/token';
import { PrismaService } from '@/src/prisma.service';
import { EventsService } from '@/src/navigator/events/events.service';
import { PagesService } from '@/src/navigator/pages/pages.service';
import { MenusService } from '@/src/navigator/menus/menu.service';
import { includes } from 'zod';
@Injectable()
export class SelectService {
async run(dto: userSelectValidator) {
return dto;
constructor(
private readonly redis: RedisHandlers,
private readonly prisma: PrismaService,
private readonly pagesService: PagesService,
private readonly eventService: EventsService,
private readonly menusService: MenusService,
) { }
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];
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.uuid }, omit: { id: true } });
const staff = await this.prisma.staff.findFirstOrThrow({
where: { uu_id: employee.staff_uu_id },
select: {
uu_id: true,
staff_code: true,
user_type_id: true,
duties_id: true,
staff_name: true,
staff_description: true,
duties_uu_id: true,
created_credentials_token: true,
updated_credentials_token: true,
confirmed_credentials_token: true,
is_confirmed: true,
deleted: true,
active: true,
is_notification_send: true,
is_email_send: true,
expiry_starts: true,
expiry_ends: true,
created_at: true,
updated_at: true,
ref_int: true,
user_types: {
select: {
token: true,
type_token: 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 staffUserType = staff.user_type_id ?
await this.prisma.user_types.findFirst({
where: { id: staff.user_type_id },
select: {
token: true,
type_token: true
}
}) : null;
const employeeToken = EmployeeTokenSchema.parse({
uuid: dto.uuid,
company: company,
department: department,
duty: duty,
employee: employee,
staff: staff,
menu: null,
pages: null,
events: null,
selection: await this.prisma.employees.findFirstOrThrow({
where: { uu_id: dto.uuid },
select: {
uu_id: true,
staff: {
select: {
uu_id: true,
staff_code: true,
user_types: {
select: {
uu_id: true,
token: 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,
},
},
},
},
},
},
},
},
},
},
},
}),
typeToken: staffUserType?.type_token,
functionsRetriever: staffUserType?.token,
kind: UserType.employee,
});
// Render page and menu
// const collection = this.mongoService.getDb(`Events/${company.uu_id}`)
const events = ""
const tokenSelect = await this.redis.setSelectToRedis(
accessToken,
employeeToken,
accessObject.value.users.uu_id,
dto.uuid,
);
return { message: 'Select successful', token: tokenSelect };
} else if (userType === 'occupant') {
const livingSpace = await this.prisma.build_living_space.findFirstOrThrow({
where: { uu_id: dto.uuid },
select: {
uu_id: true,
build_parts_uu_id: true,
occupant_type_uu_id: true,
occupant_types: {
select: {
user_types: {
select: {
token: true,
type_token: true
}
}
}
}
}
});
const occupantType = await this.prisma.occupant_types.findFirstOrThrow({
where: { uu_id: livingSpace.occupant_type_uu_id }
});
const userTypeInfo = occupantType.user_type_uu_id ?
await this.prisma.user_types.findFirst({
where: { uu_id: occupantType.user_type_uu_id },
select: {
uu_id: true,
type: true,
description: true,
type_token: true,
token: true
}
}) : null;
const part = await this.prisma.build_parts.findFirstOrThrow({
where: { uu_id: livingSpace.build_parts_uu_id },
select: {
uu_id: true,
part_code: true,
part_no: true,
part_level: true,
human_livable: true,
build_uu_id: true,
api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown: {
select: {
uu_id: true,
enum_class: true,
value: true
}
}
}
});
const build = await this.prisma.build.findFirstOrThrow({
where: { uu_id: part.build_uu_id },
select: {
uu_id: true,
build_name: true
}
});
const company = await this.prisma.companies.findFirstOrThrow({
where: { uu_id: accessObject.value.users.related_company },
select: {
uu_id: true,
is_confirmed: true,
deleted: true,
active: true,
created_at: true,
updated_at: true,
ref_int: true
}
});
const occupantToken = OccupantTokenSchema.parse({
uuid: dto.uuid,
livingSpace: livingSpace,
occupant: occupantType,
build: build,
part: part,
company: company,
menu: null,
pages: null,
events: null,
selection: {
occupant_types: {
uu_id: occupantType.uu_id,
occupant_code: occupantType.occupant_code,
occupant_type: occupantType.occupant_type
},
build_parts: {
uu_id: part.uu_id,
part_code: part.part_code,
part_no: part.part_no,
part_level: part.part_level,
human_livable: part.human_livable,
api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown: {
uu_id: part.api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown.uu_id,
enum_class: part.api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown.enum_class,
value: part.api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown.value
},
build: {
uu_id: build.uu_id, build_name: build.build_name
}
}
},
typeToken: userTypeInfo?.type_token,
functionsRetriever: userTypeInfo?.token,
kind: UserType.occupant
});
occupantToken.events = await this.eventService.getEventsOccupants(livingSpace.uu_id);
occupantToken.pages = await this.pagesService.getPagesOccupants(accessObject.value.users.uu_id, livingSpace.occupant_types.user_types?.token || '');
occupantToken.menu = await this.menusService.renderOccupantMenu(occupantToken.pages);
const tokenSelect = await this.redis.setSelectToRedis(accessToken, occupantToken, accessObject.value.users.uu_id, dto.uuid);
return { message: 'Select successful', token: tokenSelect };
} else { throw new NotAcceptableException('Invalid user type') }
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { MongoService } from './mongo.service';
import { MongoProvider } from './mongo.provider';
@Module({
providers: [MongoProvider, MongoService],
exports: [MongoService, MongoProvider, 'MONGO_DB']
})
export class MongoModule {}

View File

@@ -0,0 +1,11 @@
import { MongoClient, Db } from 'mongodb';
export const MongoProvider = {
provide: 'MONGO_DB',
useFactory: async (): Promise<Db> => {
const uri = 'mongodb://appuser:apppassword@10.10.2.13:27017/appdb';
const client = new MongoClient(uri);
await client.connect();
return client.db('appdb');
},
};

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MongoService } from './mongo.service';
describe('MongoService', () => {
let service: MongoService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [MongoService],
}).compile();
service = module.get<MongoService>(MongoService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,294 @@
import { Injectable, Inject } from '@nestjs/common';
import { Db, Document, Collection, Filter, ObjectId, UpdateResult, Sort, SortDirection } from 'mongodb';
@Injectable()
export class MongoService {
private collection: Collection<Document>;
constructor(@Inject('MONGO_DB') private readonly db: Db) { this.collection = this.db.collection('mongoCache') }
async set(collectionName: string) { this.collection = this.db.collection(collectionName) }
async getDb() { return this.collection }
async create(data: Record<string, any>): Promise<Document> {
const insertResult = await this.collection.insertOne(data);
if (!insertResult.acknowledged) { throw new Error('Failed to insert document') }
return await this.getOne(insertResult.insertedId);
}
async createMany(data: Record<string, any>[]): Promise<number> {
const insertResult = await this.collection.insertMany(data);
if (!insertResult.acknowledged) { throw new Error('Failed to insert documents') }
return insertResult.insertedCount;
}
async findManyKeyWhere(value: string, limit?: number, skip?: number): Promise<Document[]> {
const docs = await this.collection.find({
$where: function () {
return Object.keys(this).some(key => key.includes(value));
}
}).toArray();
return docs;
}
async findManyKeyWhereValue(value: string, limit?: number, skip?: number): Promise<Document[]> {
const docs = await this.collection.find({
$where: function () {
return Object.keys(this).some(key => this[key] === value);
}
}).toArray();
return docs;
}
/**
* Find a document by UUID or create it if it doesn't exist
* @param data Document data with UUID field
* @returns The found or created document
*
* @example
* Create a new user or retrieve existing one
* const userData = { uuid: 'TOKEN:12345:user', name: 'John Doe', email: 'john@example.com' };
* const user = await mongoService.findOrCreate(userData);
*/
async findOrCreate(data: Record<string, any>): Promise<{ data: Document, isCreated: boolean }> {
if (!data.uuid) { throw new Error('UUID is required for findOrCreate operation') }
// Use direct UUID lookup instead of regex for exact match
const existingDoc = await this.collection.findOne({ uuid: data.uuid } as Filter<Document>);
if (existingDoc) { return { data: existingDoc, isCreated: false } }
const insertResult = await this.collection.insertOne(data);
if (!insertResult.acknowledged) { throw new Error('Failed to insert document') }
return { data: await this.getOne(insertResult.insertedId), isCreated: true };
}
/**
* Get all documents from the collection
* @returns Array of all documents
*
* @example
* Get all users in the collection
* const allUsers = await mongoService.getAll();
*/
async getAll(): Promise<Document[]> { return await this.collection.find().toArray() }
/**
* Find a document by ID key using regex pattern
* @param idKey ID key to search for
* @returns The found document
* @throws Error if document is not found
*
* @example
* Find a user by ID key
* const user = await mongoService.findOne('12345');
* This will search for documents with uuid matching pattern ^TOKEN:12345:
*/
async findOne(filter: Filter<Document>): Promise<Document | null> {
const result = await this.collection.findOne(filter);
return result ? result : null;
}
/**
* Find multiple documents using a filter
* @param filter MongoDB filter
* @param limit Optional limit of results (default: no limit)
* @param skip Optional number of documents to skip (default: 0)
* @returns Array of matching documents
*
* @example
* Find active users with pagination
* const filter = { active: true } as Filter<Document>;
* const activeUsers = await mongoService.findMany(filter, 10, 20); // limit 10, skip 20
*
* @example
* Find users by role
* const admins = await mongoService.findMany({ role: 'admin' } as Filter<Document>);
* const documents = await mongoService.findMany(filter, limit, skip, ['name', 'createdAt'], ['asc', 'desc']);
*/
async findMany(filter: Filter<Document>, limit?: number, skip?: number, sortBys?: string[], sortDirections?: SortDirection[]): Promise<Document[]> {
let query = this.collection.find(filter);
if (typeof skip === 'number') { query = query.skip(skip) }
if (typeof limit === 'number') { query = query.limit(limit) }
if (sortBys && sortDirections && sortBys.length === sortDirections.length) {
const sortOptions = sortBys.reduce<Record<string, SortDirection>>((acc, sortBy, index) => ({
...acc,
[sortBy]: sortDirections[index]
}), {});
query = query.sort(sortOptions);
}
return await query.toArray();
}
/**
* Get a document by its MongoDB ObjectId
* @param id MongoDB ObjectId
* @returns The found document
* @throws Error if document is not found
*
* @example
* Get a user by ObjectId
* const userId = new ObjectId('507f1f77bcf86cd799439011');
* const user = await mongoService.getOne(userId);
*/
async getOne(id: ObjectId): Promise<Document> {
const result = await this.collection.findOne({ _id: id });
if (!result) { throw new Error(`Document with ID ${id.toString()} not found`) }
return result;
}
/**
* Find documents by regex pattern on UUID field
* @param idKey ID key to search for
* @returns Array of matching documents
*
* @example
* Find all users with a specific ID key pattern
* const users = await mongoService.findByRegex('12345');
* This will return all documents with uuid matching pattern ^TOKEN:12345:
*/
async findByRegex(idKey: string): Promise<Document[]> {
if (!idKey) { throw new Error('ID key is required for regex search') }
const pattern = `^${idKey}`;
return await this.collection.find({ uuid: { $regex: pattern } } as Filter<Document>).toArray();
}
/**
* Update a single document by its MongoDB ObjectId
* @param id MongoDB ObjectId
* @param data Data to update
* @returns The updated document
* @throws Error if document is not found or update fails
*
* @example
* Update a user's profile
* const userId = new ObjectId('507f1f77bcf86cd799439011');
* const updates = { name: 'Jane Doe', lastLogin: new Date() };
* const updatedUser = await mongoService.updateOne(userId, updates);
*/
async updateOne(id: ObjectId, data: Record<string, any>): Promise<Document> {
const updateResult = await this.collection.updateOne(
{ _id: id },
{ $set: data }
);
if (!updateResult.acknowledged) { throw new Error('Update operation failed') }
if (updateResult.matchedCount === 0) { throw new Error(`Document with ID ${id.toString()} not found`) }
return await this.getOne(id);
}
/**
* Update multiple documents matching a filter
* @param filter MongoDB filter
* @param data Data to update
* @returns Update result with count of modified documents
* @throws Error if update fails
*
* @example
* Mark all inactive users as archived
* const filter = { active: false } as Filter<Document>;
* const updates = { status: 'archived', archivedAt: new Date() };
* const result = await mongoService.updateMany(filter, updates);
* console.log(`${result.modifiedCount} users archived`);
*/
async updateMany(filter: Filter<Document>, data: Record<string, any>): Promise<UpdateResult> {
const updateResult = await this.collection.updateMany(filter, { $set: data });
if (!updateResult.acknowledged) { throw new Error('Update operation failed') }
return updateResult;
}
/**
* Delete a document by its MongoDB ObjectId
* @param id MongoDB ObjectId
* @returns True if document was deleted, false otherwise
*
* @example
* Delete a user account
* const userId = new ObjectId('507f1f77bcf86cd799439011');
* const deleted = await mongoService.deleteOne(userId);
* if (deleted) console.log('User successfully deleted');
*/
async deleteOne(id: ObjectId): Promise<boolean> {
const deleteResult = await this.collection.deleteOne({ _id: id });
return deleteResult.acknowledged && deleteResult.deletedCount > 0;
}
/**
* Delete multiple documents matching a filter
* @param filter MongoDB filter
* @returns Number of deleted documents
*
* @example
* Delete all expired sessions
* const filter = { expiresAt: { $lt: new Date() } } as Filter<Document>;
* const count = await mongoService.deleteMany(filter);
* console.log(`${count} expired sessions deleted`);
*/
async deleteMany(filter: Filter<Document>): Promise<number> {
const deleteResult = await this.collection.deleteMany(filter);
return deleteResult.acknowledged ? deleteResult.deletedCount : 0;
}
/**
* Find documents by regex pattern on any specified field
* @param field The field name to apply the regex filter on
* @param value The value to search for in the field
* @param options Optional regex options (e.g., 'i' for case-insensitive)
* @param prefix Optional prefix to add before the value (default: '')
* @param suffix Optional suffix to add after the value (default: '')
* @returns Array of matching documents
*
* @example
* Find users with email from a specific domain (case-insensitive)
* const gmailUsers = await mongoService.findByFieldRegex('email', 'gmail.com', 'i');
*
* @example
* Find users with names starting with 'J'
* const usersStartingWithJ = await mongoService.findByFieldRegex('name', 'J', 'i', '^');
*
* @example
* Find users with phone numbers ending in specific digits
* const specificPhoneUsers = await mongoService.findByFieldRegex('phone', '5555', '', '', '$');
*/
async findByFieldRegex(field: string, value: string, options?: string, prefix: string = '', suffix: string = ''): Promise<Document[]> {
if (!field || !value) { throw new Error('Field name and value are required for regex search') }
const pattern = `${prefix}${value}${suffix}`;
const query: Record<string, any> = {};
query[field] = { $regex: pattern };
if (options) { query[field].$options = options; }
return await this.collection.find(query as unknown as Filter<Document>).toArray();
}
/**
* Find documents by regex pattern across all fields (including nested)
* @param value The value to search for
* @param options Optional regex options (e.g., 'i' for case-insensitive)
* @returns Array of matching documents
*
* @example
* Find any document containing a specific value anywhere
* const docs = await mongoService.findByRegexAcrossFields('someValue', 'i');
*/
async findByRegexAcrossFields(value: string, options?: string, searchType: 'value' | 'key' | 'both' = 'value'): Promise<Document[]> {
if (!value) { throw new Error('Search value is required') }
const query: any = { $or: [] };
if (searchType === 'value' || searchType === 'both') {
query.$or.push(
{ '$expr': { $regexMatch: { input: { $toString: '$$ROOT' }, regex: value, options } } },
{ 'data': { $type: 'object', $regex: value, $options: options } }
);
}
if (searchType === 'key' || searchType === 'both') {
query.$where = function () {
const searchRegex = new RegExp(value, options);
function checkKeys(obj: Record<string, any>) {
for (const key in obj) {
if (searchRegex.test(key)) return true; if (obj[key] && typeof obj[key] === 'object') { if (checkKeys(obj[key])) return true }
}
return false;
} return checkKeys(this)
}.toString();
}
return await this.collection.find(query as unknown as Filter<Document>).toArray();
}
}

View File

@@ -0,0 +1,2 @@
export const REDIS_CLIENT = 'REDIS_CLIENT';
export const REDIS_OPTIONS = 'REDIS_OPTIONS';

View File

@@ -0,0 +1,23 @@
import { ModuleMetadata, Type } from '@nestjs/common';
export interface RedisModuleOptions {
config?: {
host?: string;
port?: number;
password?: string;
db?: number;
keyPrefix?: string;
[key: string]: any;
};
}
export interface RedisOptionsFactory {
createRedisOptions(): Promise<RedisModuleOptions> | RedisModuleOptions;
}
export interface RedisModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
useExisting?: Type<RedisOptionsFactory>;
useClass?: Type<RedisOptionsFactory>;
useFactory?: (...args: any[]) => Promise<RedisModuleOptions> | RedisModuleOptions;
inject?: any[];
}

View File

@@ -0,0 +1,91 @@
import Redis from 'ioredis';
import { DynamicModule, Module, OnApplicationShutdown, Provider } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { CacheService } from './redis.service';
import { REDIS_CLIENT, REDIS_OPTIONS } from './redis.constants';
import { RedisModuleOptions, RedisModuleAsyncOptions } from './redis.interfaces';
@Module({})
export class RedisModule implements OnApplicationShutdown {
constructor(private moduleRef: ModuleRef) { }
/**
* Registers the module with default configuration.
*
* @param isGlobal - Register in the global scope
* @returns A DynamicModule
*/
static forRootWithConfig(isGlobal = false): DynamicModule {
const redisConfig = { host: '10.10.2.15', port: 6379, password: 'your_strong_password_here' };
return RedisModule.forRoot({ config: redisConfig }, isGlobal);
}
/**
* Registers the module synchronously.
*
* @param options - The module options
* @param isGlobal - Register in the global scope
* @returns A DynamicModule
*/
static forRoot(options?: RedisModuleOptions, isGlobal = false): DynamicModule {
const redisOptionsProvider: Provider = {
provide: REDIS_OPTIONS,
useValue: options || {},
};
const redisClientProvider: Provider = {
provide: REDIS_CLIENT,
useFactory: () => {
if (!options || !options.config) { return new Redis() }
const { host, port, password } = options.config;
return new Redis({ host, port, password });
},
};
return {
module: RedisModule,
providers: [redisOptionsProvider, redisClientProvider, CacheService],
exports: [CacheService],
global: isGlobal,
};
}
/**
* Registers the module asynchronously.
*
* @param options - The async module options
* @param isGlobal - Register in the global scope
* @returns A DynamicModule
*/
static forRootAsync(options: RedisModuleAsyncOptions, isGlobal = false): DynamicModule {
const redisOptionsProvider: Provider = {
provide: REDIS_OPTIONS,
useFactory: options.useFactory || (() => ({})),
inject: options.inject || [],
};
const redisClientProvider: Provider = {
provide: REDIS_CLIENT,
useFactory: (redisOptions: RedisModuleOptions) => {
if (!redisOptions || !redisOptions.config) { return new Redis() }
const { host, port, password } = redisOptions.config;
return new Redis({ host, port, password })
},
inject: [REDIS_OPTIONS],
};
return {
module: RedisModule,
imports: options.imports || [],
providers: [redisOptionsProvider, redisClientProvider, CacheService],
exports: [CacheService],
global: isGlobal,
};
}
async onApplicationShutdown() {
const client = this.moduleRef.get(REDIS_CLIENT, { strict: false });
if (client && typeof client.quit === 'function') { await client.quit() }
}
}

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CacheService } from './redis.service';
import Redis from 'ioredis';
import { REDIS_CLIENT } from './redis.constants';
describe('CacheService', () => {
let service: CacheService;
let mockRedisClient: Redis;
beforeEach(async () => {
// Create a mock Redis client
mockRedisClient = new Redis();
const module: TestingModule = await Test.createTestingModule({
providers: [
CacheService,
{
provide: REDIS_CLIENT,
useValue: mockRedisClient,
},
],
}).compile();
service = module.get<CacheService>(CacheService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -1,13 +1,13 @@
import { Injectable } from '@nestjs/common';
import { RedisService } from '@liaoliaots/nestjs-redis';
import Redis from 'ioredis';
import { Injectable, Inject } from '@nestjs/common';
import { REDIS_CLIENT } from './redis.constants';
@Injectable()
export class CacheService {
private client: Redis;
constructor(private readonly redisService: RedisService) {
this.client = this.redisService.getOrThrow();
constructor(@Inject(REDIS_CLIENT) private readonly redisClient: Redis) {
this.client = redisClient;
}
async set(key: string, value: any) {
@@ -16,19 +16,8 @@ export class CacheService {
async get(key: string): Promise<any | null> {
const value = await this.client.get(key);
if (!value) {
return null;
}
return JSON.parse(value);
}
async get_with_keys(listKeys: (string | null)[]): Promise<any | null> {
const joinKeys = this.createRegexPattern(listKeys);
const value = await this.client.get(joinKeys);
if (!value) {
return null;
}
return JSON.parse(value);
if (!value) { return null }
return { key, value: JSON.parse(value) }
}
async set_with_ttl(key: string, value: any, ttl: number) {
@@ -41,10 +30,7 @@ export class CacheService {
async delete(key: string): Promise<boolean> {
const deleted = await this.client.del(key);
if (deleted === 0) {
return false;
}
return true;
return deleted === 0 ? false : true;
}
/**

View File

@@ -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));
}

View File

@@ -4,46 +4,39 @@ import {
Injectable,
ForbiddenException,
} from '@nestjs/common';
import { RedisHandlers } from '@/src/utils/auth/redis_handlers';
import { RedisHandlers } from '@/src/utils/store/redisHandlers';
import { UrlHandler } from '@/src/utils/navigator/urlHandler';
@Injectable()
export class AuthControlGuard implements CanActivate {
constructor(private cacheService: RedisHandlers) {}
constructor(private cacheService: RedisHandlers) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
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');
// }
if (!accessToken) { throw new ForbiddenException('Send to Login') }
this.cacheService.renewTtlLoginFromRedis(req);
return true;
}
}
@Injectable()
export class EndpointControlGuard implements CanActivate {
constructor(private cacheService: RedisHandlers) {}
constructor(
private cacheService: RedisHandlers,
private urlHandler: UrlHandler,
) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
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 keyUrl = `${path}:${method.toUpperCase()}`;
const driveToken = await this.urlHandler.getSecureUrlToken(keyUrl);
const accessObject = await this.cacheService.getSelectFromRedis(req);
if (!accessObject) { throw new ForbiddenException('Access denied') }
req.driveToken = `${driveToken}:${accessObject?.value.functionsRetriever}`;
this.cacheService.renewTtlSelectFromRedis(req);
return true;
}
}

View File

@@ -0,0 +1,47 @@
import { IsString, IsObject, IsOptional, IsNumber } from 'class-validator';
export class mongoSetValidator {
@IsString()
collectionName: string;
@IsObject()
data: object;
}
export class mongoGetValidator {
@IsString()
collectionName: string;
@IsString()
@IsOptional()
regexKey: string;
@IsObject()
@IsOptional()
filter: object;
@IsNumber()
@IsOptional()
limit: number;
@IsNumber()
@IsOptional()
skip: number;
}
export class eventSetValidator {
@IsString()
usersUUID: string;
@IsObject()
event: Record<string, Record<string, Record<string, string>>>;
}
export class eventGetValidator {
@IsString()
usersUUID: string;
@IsString()
typeToken: string;
}

View File

@@ -0,0 +1,41 @@
import { IsString, IsObject, IsOptional, IsNumber, ValidateNested } from 'class-validator';
// { # collection Events:Build-UUIDv4 | Events:Company-UUIDv4 : "userUUID" : { "userTypeToken" : { "siteUrlToken" : "eventKey" } } }
// const jsonData = { 'USER-UUID(V4)': { 'j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA': { 'e6hewIe7YqbQZHO3': 'qt5P0xoeThjNT9EuWfwBgxsntHY5ydRtKFr1pgKGcgxx' } } };
export class EventsSetterValidator {
@IsObject()
event: Record<string, Record<string, Record<string, string>>>;
@IsString()
userUUID: string; // UUID of employee or occupant
}
export class EventsGetterValidator {
@IsString()
collectionName: string;
@IsString()
dutyUUID: string; // UUID of employee or occupant
@IsString()
@IsOptional()
regexKey?: string;
@IsString()
@IsOptional()
searchType?: 'value' | 'key' | 'both';
@IsObject()
@IsOptional()
filter?: object = {};
@IsNumber()
@IsOptional()
limit?: number = 1;
@IsNumber()
@IsOptional()
skip?: number = 0;
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EventsService } from './events.service';
describe('EventsService', () => {
let service: EventsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EventsService],
}).compile();
service = module.get<EventsService>(EventsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,214 @@
import { Body, Injectable, NotFoundException } from '@nestjs/common';
import { MongoService } from '@/src/database/mongo/mongo.service';
import { PrismaService } from '@/src/prisma.service';
import { EventsGetterValidator, EventsSetterValidator } from '@/src/navigator/events/dtoValidator';
import { Document } from 'mongodb';
type SearchType = 'value' | 'key' | 'both';
@Injectable()
export class EventsService {
// const result = await eventsService.getEventsOccupants({
// collectionName: 'Events:Build-UUIDv4',
// regexKey: 'qt5P0xoeThjNT9EuWfwBgxsntHY5ydRtKFr1pgKGcgxx'
// });
// const result = await eventsService.getEventsOccupants({
// collectionName: 'Events:Build-UUIDv4',
// regexKey: 'e6hewIe7YqbQZHO3',
// searchType: 'key'
// });
// const result = await eventsService.getEventsOccupants({
// collectionName: 'Events:Build-UUIDv4',
// regexKey: 'e6hewIe7YqbQZHO3',
// searchType: 'both'
// });
constructor(private mongoService: MongoService, private prisma: PrismaService) { }
seperator = "/"
private async getBuildUUID(uuid: string) {
console.log('uuid', uuid)
const livingSpace = await this.prisma.build_living_space.findFirstOrThrow({
where: { uu_id: uuid },
select: {
people: {
select: {
users: {
select: {
uu_id: true
}
}
}
},
build_parts: {
select: {
build: {
select: {
uu_id: true
}
}
}
}
}
});
const userUUID = livingSpace.people.users[0].uu_id
const buildUUID = livingSpace.build_parts.build.uu_id
return { userUUID, buildUUID }
}
private async getCompanyUUID(uuid: string) {
const employee = await this.prisma.employees.findFirstOrThrow({
where: { uu_id: uuid },
select: {
people: {
select: {
users: {
select: {
uu_id: true
}
}
}
},
staff: {
select: {
duties: {
select: {
company_uu_id: true
}
}
}
}
}
});
const userUUID = employee.people?.users[0].uu_id
const companyUUID = employee.staff?.duties.company_uu_id
return { userUUID, companyUUID }
}
private validateCollectionName(collectionName: string) {
if (!collectionName) {
throw new NotFoundException('Collection name is required')
}
}
private async setupMongoCollection(collectionName: string, buildUUID: string) {
await this.mongoService.set(collectionName);
await this.mongoService.set(`EVENTS${this.seperator}${buildUUID}`);
}
private escapeRegex(text: string): string {
return text.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
}
async getAllEventsOccupants(userTypeToken: string): Promise<Document[] | null> {
await this.mongoService.set(`Events`)
return await this.mongoService.findManyKeyWhere(userTypeToken) || null;
}
async getAllEventsEmployees(userTypeToken: string): Promise<Document[] | null> {
await this.mongoService.set(`Events`)
return await this.mongoService.findManyKeyWhere(userTypeToken) || null;
}
async getEventsOccupants(livingSpaceUUID: string) {
const eventsObject = {}
const { userUUID, buildUUID } = await this.getBuildUUID(livingSpaceUUID);
const collectionKey = `Events/${buildUUID}`
this.validateCollectionName(collectionKey);
await this.mongoService.set(collectionKey)
const eventsResponse = await this.mongoService.findOne({ [userUUID]: { $exists: true } });
if (eventsResponse && typeof eventsResponse === 'object') {
const mapOfEvents = eventsResponse[userUUID];
if (mapOfEvents && typeof mapOfEvents === 'object') {
const userTypeTokenKey = Object.keys(mapOfEvents)[0];
const userTypeTokenValue = mapOfEvents[userTypeTokenKey];
if (userTypeTokenValue && typeof userTypeTokenValue === 'object') {
for (const siteUrlTokenKey of Object.keys(userTypeTokenValue)) {
const siteUrlTokenValue = userTypeTokenValue[siteUrlTokenKey];
eventsObject[`${siteUrlTokenKey}:${userTypeTokenKey}`] = siteUrlTokenValue
}
}
}
}
return eventsObject || null;
}
async getEventsEmployees(employeeUUID: string) {
const eventsObject = {}
const companyUUID = await this.getCompanyUUID(employeeUUID);
if (!companyUUID) { throw new NotFoundException('Company not found') }
await this.mongoService.set(`EVENTS${this.seperator}${companyUUID}`);
const eventsResponse = await this.mongoService.findOne({ [employeeUUID]: { $exists: true } });
if (eventsResponse && typeof eventsResponse === 'object') {
const mapOfEvents = eventsResponse[employeeUUID];
if (mapOfEvents && typeof mapOfEvents === 'object') {
const userTypeTokenKey = Object.keys(mapOfEvents)[0];
const userTypeTokenValue = mapOfEvents[userTypeTokenKey];
if (userTypeTokenValue && typeof userTypeTokenValue === 'object') {
for (const siteUrlTokenKey of Object.keys(userTypeTokenValue)) {
const siteUrlTokenValue = userTypeTokenValue[siteUrlTokenKey];
eventsObject[`${siteUrlTokenKey}:${userTypeTokenKey}`] = siteUrlTokenValue
}
}
}
}
return eventsObject || null;
}
private async setSavedEventToMapper(data: any, useruuid: string) {
await this.mongoService.set(`MAP${this.seperator}EVENTS`);
const events = await this.mongoService.findOrCreate({ uuid: `EVENTS${this.seperator}${useruuid}:${data.uuid}`, data });
}
private async deleteSavedEventFromMapper(data: any, useruuid: string) {
await this.mongoService.set(`MAP${this.seperator}EVENTS`);
const events = await this.mongoService.deleteMany({ uuid: `EVENTS${this.seperator}${useruuid}:${data.uuid}` });
return events;
}
async setEventsEmployees(@Body() body: EventsSetterValidator) {
const companyUUID = await this.getCompanyUUID(body.userUUID);
if (!companyUUID) { throw new NotFoundException('Company not found') }
await this.mongoService.set(`EVENTS${this.seperator}${companyUUID}`);
const events = await this.mongoService.findOrCreate(body.event);
// await this.setSavedEventToMapper(events, body.dutyUUID);
return events;
}
async setEventsOccupants(@Body() body: EventsSetterValidator) {
const buildUUID = await this.getBuildUUID(body.userUUID);
if (!buildUUID) { throw new NotFoundException('Build not found') }
await this.mongoService.set(`EVENTS${this.seperator}${buildUUID}`);
const events = await this.mongoService.findOrCreate(body.event);
return events;
}
async setEvents(events: any, serviceName: string) {
await this.mongoService.set(`Events`);
for (const [key, value] of Object.entries(events)) {
const description = (value as Array<any>)[0].endpoint || "";
console.log(`Setting events for ${serviceName} ${description} is carried to nosql database store.`)
await this.mongoService.deleteMany({ [key]: { $exists: true } });
await this.mongoService.create({ [key]: value });
}
}
async deleteEventsEmployees(@Body() body: EventsGetterValidator) {
const companyUUID = await this.getCompanyUUID(body.dutyUUID);
if (!companyUUID) { throw new NotFoundException('Company not found') }
await this.mongoService.set(`EVENTS${this.seperator}${companyUUID}`);
const events = await this.mongoService.deleteMany({ uuid: { $regex: body.regexKey, $options: 'i' } });
return events;
}
async deleteEventsOccupants(@Body() body: EventsGetterValidator) {
const buildUUID = await this.getBuildUUID(body.dutyUUID);
if (!buildUUID) { throw new NotFoundException('Build not found') }
await this.mongoService.set(`EVENTS${this.seperator}${buildUUID}`);
const events = await this.mongoService.deleteMany({ uuid: { $regex: body.regexKey, $options: 'i' } });
return events;
}
}

View File

@@ -0,0 +1,37 @@
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { EventsSetterValidator } from './dtoValidator';
// Example JSON data to validate
const jsonData = {
collectionName: 'Events:Build-UUIDv4',
data: {
'USER-UUID(V4)': {
'j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA': {
'e6hewIe7YqbQZHO3': 'qt5P0xoeThjNT9EuWfwBgxsntHY5ydRtKFr1pgKGcgxx'
}
}
}
};
async function validateEventData() {
const eventData = plainToClass(EventsSetterValidator, jsonData);
const errors = await validate(eventData, {
whitelist: true,
forbidNonWhitelisted: true,
validationError: { target: false }
});
if (errors.length > 0) {
console.log('Validation failed. Errors:', JSON.stringify(errors, null, 2));
return false;
} else {
console.log('Validation successful!');
console.log('Validated data:', JSON.stringify(eventData, null, 2));
return true;
}
}
validateEventData()
.then(isValid => console.log('Is valid:', isValid))
.catch(err => console.error('Error during validation:', err));

View File

@@ -0,0 +1,48 @@
import { interfaceMenu, interfaceMapper, interfaceMenus } from "@/src/utils/types/menus";
import { menuForEmployeeDefinition } from "./menuItems/employee";
import { menuForOccupantDefinition } from "./menuItems/occupant";
const config = {
FirstLayerColor: "#ebc334",
SecondLayerColor: "#18910d",
ThirdLayerColor: "#2825c4",
employeePrefix: "/office",
occupantPrefix: "/venue"
}
function generateMapperKey(keys: string[]): string { return keys.join(':') + ':' }
function generateMapper(menu: interfaceMenu[], parentKeys: string[] = []): interfaceMapper {
let mapper: interfaceMapper = {};
for (const item of menu) {
const currentKeys = [...parentKeys, item.key];
if (item.page) { mapper[item.page] = generateMapperKey(currentKeys) }
if (item.subs) { const subMapper = generateMapper(item.subs, currentKeys); mapper = { ...mapper, ...subMapper } }
}
return mapper;
}
function generateDynamicMapper(menus: interfaceMenu[]): interfaceMapper { return generateMapper(menus) }
function applyColorsAndPrefixes(menu: interfaceMenu[], isEmployee: boolean, config: any, layer: number = 1): interfaceMenu[] {
return menu.map(item => {
const newItem = { ...item };
if (layer === 1) newItem.color = config.FirstLayerColor;
else if (layer === 2) newItem.color = config.SecondLayerColor;
else if (layer >= 3) newItem.color = config.ThirdLayerColor;
if (newItem.page) { newItem.page = `${isEmployee ? config.employeePrefix : config.occupantPrefix}${newItem.page}` }
if (newItem.subs) { newItem.subs = applyColorsAndPrefixes(newItem.subs, isEmployee, layer + 1) }
return newItem;
});
}
export const occupantMenus: interfaceMenus = {
Menu: applyColorsAndPrefixes(menuForOccupantDefinition, false, config),
Mapper: generateDynamicMapper(applyColorsAndPrefixes(menuForOccupantDefinition, false, config))
};
export const employeeMenus: interfaceMenus = {
Menu: applyColorsAndPrefixes(menuForEmployeeDefinition, true, config),
Mapper: generateDynamicMapper(applyColorsAndPrefixes(menuForEmployeeDefinition, true, config))
};

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ServicesService } from './menu.service';
describe('ServicesService', () => {
let service: ServicesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ServicesService],
}).compile();
service = module.get<ServicesService>(ServicesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import { employeeMenus, occupantMenus } from "./main";
import { interfaceMenu } from "@/src/utils/types/menus";
// backend_service | Occupant pages
// backend_service | {
// backend_service | IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE: 'IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE:hES1KfaPRZeadmmjdryShA'
// backend_service | }
// backend_service | Occupant Menu Structure
// backend_service | Occupant pages
// backend_service | [
// backend_service | {
// backend_service | key: 'dzFGPzZJRgmft4HrrTeBtQ',
// backend_service | icon: '',
// backend_service | text: { tr: 'Pano', en: 'Dashboard' },
// backend_service | page: '/venue/dashboard',
// backend_service | token: 'IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE',
// backend_service | color: '#ebc334',
// backend_service | subs: []
// backend_service | }
// backend_service | ]
// backend_service | [
// backend_service | {
// backend_service | key: 'dzFGPzZJRgmft4HrrTeBtQ',
// backend_service | icon: '',
// backend_service | text: { tr: 'Pano', en: 'Dashboard' },
// backend_service | page: '/venue/dashboard',
// backend_service | token: 'IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE',
// backend_service | color: '#ebc334',
// backend_service | subs: []
// backend_service | }
// backend_service | ]
@Injectable()
export class MenusService {
constructor() { }
async renderOccupantMenu(pages: any) {
const defaultMenu: interfaceMenu[] = occupantMenus.Menu;
const userHasUrls = Object.keys(pages)
const renderedMenu: interfaceMenu[] = [];
defaultMenu.map((value) => { if (value.token && userHasUrls.includes(value.token)) { renderedMenu.push(value as interfaceMenu) } });
return renderedMenu || [];
}
async renderEmployeeMenu(pages: Record<string, string>) {
const defaultMenu = employeeMenus.Menu;
console.log('Employee pages');
console.dir(pages);
console.log('Employee Menu Structure');
console.dir(defaultMenu);
return null;
}
}

View File

@@ -0,0 +1,301 @@
export const menuForEmployeeDefinition = [
{
key: "a6EoBlTPSgGbUQELbyRwMA",
icon: "",
text: { "tr": "Dashboard", "en": "Dashboard" },
page: "/dashboard",
token: "qY56XMEr08wJkNvOR6EYQZKMVdTQEfHdLXGzzxcKU24E",
color: "",
subs: null,
},
{
key: "NV2kI8NERmqrNgIeiUYojQ",
icon: "",
text: { "tr": "Bireysel", "en": "Individual" },
page: null,
token: null,
color: "",
subs: [
{
key: "xnhFAyi3Sp2qVWcVcR6m9w",
icon: "",
text: { "tr": "Birey", "en": "Person" },
page: "/person",
token: null,
color: "",
subs: [
{
key: "7wdsqwCQSmXRsRPC9GSgwx",
icon: "",
text: { "tr": "Oluştur", "en": "Create" },
page: "/person/create",
token: null,
color: "",
subs: null
},
{
key: "56O8WRP4TyC7F8bc1vjXgx",
icon: "",
text: { "tr": "Güncelle", "en": "Update" },
page: "/person/update",
token: null,
color: "",
subs: null
},
{
key: "RPaESp64SUmjNyEY1WUE8Q",
icon: "",
text: { "tr": "Sil", "en": "Delete" },
page: "/person/delete",
token: null,
color: "",
subs: null
}
]
},
{
key: "qcRK3EPQSoLSWkJFhtWOwx",
icon: "",
text: { "tr": "Kullanıcı", "en": "User" },
page: "/users",
token: null,
color: "",
subs: [
{
key: "PqNGe0SaQKeyUGyzJoSLwx",
icon: "",
text: { "tr": "Oluştur", "en": "Create" },
page: "/users/create",
token: null,
color: "",
subs: null
},
{
key: "ruvQlE7wQzqHqUvCNIoUnA",
icon: "",
text: { "tr": "Güncelle", "en": "Update" },
page: "/users/update",
token: null,
color: "",
subs: null
},
{
key: "DfDStf1dTBCRShNQeb5pZA",
icon: "",
text: { "tr": "Sil", "en": "Delete" },
page: "/users/delete",
token: null,
color: "",
subs: null
}
]
}
]
},
{
key: "ALV19bQ8S7q8LpOkdRDMwx",
icon: "",
text: { "tr": "Bina", "en": "Build" },
page: null,
token: null,
color: "",
subs: [
{
key: "eToBYS4DTEKseVYMJLNZwx",
icon: "",
text: { "tr": "Binalar", "en": "Building" },
page: null,
token: null,
color: "",
subs: [
{
key: "EkR7p6qmRN2Wb1GLsH5aEQ",
icon: "",
text: { "tr": "Oluştur", "en": "Create" },
page: "/building/build/create",
token: null,
color: "",
subs: null
},
{
key: "qcoHwABjSli04D7xeWGOHQ",
icon: "",
text: { "tr": "Güncelle", "en": "Update" },
page: "/building/build/update",
token: null,
color: "",
subs: null
},
{
key: "vC2oPkjRfudvBDlNReeRAx",
icon: "",
text: { "tr": "Sil", "en": "Delete" },
page: "/building/build/delete",
token: null,
color: "",
subs: null
}
],
},
{
key: "NFte61RnTHGPWlnoUItHAx",
icon: "",
text: { "tr": "Daireler", "en": "Parts" },
page: null,
token: null,
color: "",
subs: [
{
key: "7o6QNpelSpmxpJxTedEj4w",
icon: "",
text: { "tr": "Oluştur", "en": "Create" },
page: "/building/parts/create",
token: null,
color: "",
subs: null
},
{
key: "rP6idRkyToLcxwpalCxgxx",
icon: "OBKPalaMQwWhQmQ9Ni0y6Q",
text: { "tr": "Güncelle", "en": "Update" },
page: "/building/parts/update",
token: null,
color: "",
subs: null
},
{
key: "CBNaWzVqRaSpWaPTM54PbA",
icon: "",
text: { "tr": "Sil", "en": "Delete" },
page: "/building/parts/delete",
token: null,
color: "",
subs: null
}
],
},
{
key: "NFte61RnTHGPWlnoUItHAx",
icon: "",
text: { "tr": "Alanlar", "en": "Area" },
page: null,
token: null,
color: "",
subs: []
}
],
},
{
key: "yzvyvqMhQ06TdC9paOw4Ax",
icon: "",
text: { "tr": "Yönetim", "en": "Management" },
page: null,
token: null,
color: "",
subs: [
{
key: "DEumSZtaTSKiDsD1VJPQxx",
icon: "",
text: { "tr": "Bütçe", "en": "Budget" },
page: "/management/budget",
token: null,
color: "",
subs: [
{
key: "PIPD61aZRveFZ6GGfK3VYw",
icon: "",
text: { "tr": "Eylemler", "en": "Actions" },
page: "/management/budget/actions",
token: null,
color: "",
subs: null,
},
{
key: "",
icon: "",
text: { "tr": "Durum", "en": "Status" },
page: "/management/budget/status",
token: null,
color: "",
subs: null,
}
],
},
],
},
{
key: "RHI0bthYRjWWf4tBaPBdgx",
icon: "",
text: { "tr": "Toplantılar", "en": "Meetings" },
page: "/meetings",
token: null,
color: "",
subs: [
{
key: "OESxDOI6S4eNcdeRCrKIjQ",
icon: "",
text: { "tr": "Yıllık", "en": "Annual" },
page: "/meetings/annual",
token: null,
color: "",
subs: [
{
key: "MhEHidsRWyHdCqtHJOcvAx",
icon: "",
text: { "tr": "Oluştur", "en": "Create" },
page: "/meetings/annual/create",
token: null,
color: "",
subs: null,
},
{
key: "xhnSW4hWSDuJyREMjXOivA",
icon: "",
text: { "tr": "Kapat", "en": "Close" },
page: "/meetings/annual/close",
token: null,
color: "",
subs: null,
},
],
},
{
key: "A4raUDNFTpZ7mPfqJBGSwx",
icon: "",
text: { "tr": "Acil", "en": "Emergency" },
page: "/meetings/emergency",
token: null,
color: "",
subs: [
{
key: "T3Fd0C5Tf2V1dZhiZuNQxx",
icon: "",
text: { "tr": "Oluştur", "en": "Create" },
page: "/meetings/emergency/create",
token: null,
color: "",
subs: null,
},
{
key: "L1ogOYhSl6BDPstufiSwxx",
icon: "",
text: { "tr": "Kapat", "en": "Close" },
page: "/meetings/emergency/close",
token: null,
color: "",
subs: null,
},
],
},
{
key: "vwzmxtBoQFW62YHes5OZAg",
icon: "",
text: { "tr": "Katılımlar", "en": "Participations" },
page: "/meetings/participations",
token: null,
color: "",
subs: [],
}
],
}
];

View File

@@ -0,0 +1,13 @@
import { interfaceMenu } from "@/src/utils/types/menus";
export const menuForOccupantDefinition: interfaceMenu[] = [
{
key: "dzFGPzZJRgmft4HrrTeBtQ",
icon: "",
text: { "tr": "Pano", "en": "Dashboard" },
page: "/dashboard",
token: "IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE",
color: "",
subs: [],
},
]

View File

@@ -0,0 +1,13 @@
import { employeeMenus, occupantMenus } from './main';
console.log('Employee Menu Mapper:');
console.log(JSON.stringify(employeeMenus.Mapper, null, 2));
console.log('\nEmployee Menu Structure (with colors):');
console.log(JSON.stringify(employeeMenus.Menu, null, 2));
console.log('\nOccupant Menu Mapper:');
console.log(JSON.stringify(occupantMenus.Mapper, null, 2));
console.log('\nOccupant Menu Structure (with colors):');
console.log(JSON.stringify(occupantMenus.Menu, null, 2));

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NavigatorController } from './navigator.controller';
describe('NavigatorController', () => {
let controller: NavigatorController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [NavigatorController],
}).compile();
controller = module.get<NavigatorController>(NavigatorController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,73 @@
import { Controller, Post, NotFoundException, Body } from '@nestjs/common';
import { PrismaService } from '@/src/prisma.service';
import { EventsService } from '@/src/navigator/events/events.service';
import { MongoService } from '@/src/database/mongo/mongo.service';
import { UrlHandler } from '@/src/utils/navigator/urlHandler';
import { eventSetValidator, eventGetValidator } from './dtoValidator';
import { PagesService } from './pages/pages.service';
const tokens = { employeeTypeToken: "L9wBdwV9OlxsLAgh", occupantTypeToken: "j0adQOsJBR0xq24d" }
@Controller('navigator')
export class NavigatorController {
constructor(
private prismaService: PrismaService,
private eventService: EventsService,
private mongoService: MongoService,
private urlHandler: UrlHandler,
private pagesService: PagesService
) { }
@Post('event/set')
async setEvent(@Body() body: eventSetValidator) {
const user = await this.prismaService.users.findFirst({ where: { uu_id: body.usersUUID }, include: { people: true } });
if (!user) { throw new NotFoundException('User not found') }
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: body.event.token } })
const person = user.people[0]
const people2userType = await this.prismaService.employees.findFirstOrThrow({ where: { uu_id: person.uu_id, staff: { user_type_uu_id: userType.uu_id } } })
if (!people2userType) { throw new NotFoundException('User type not found') }
if (userType.type_token == tokens.employeeTypeToken) { await this.eventService.setEventsEmployees({ event: body.event, userUUID: body.usersUUID }) }
else if (userType.type_token == tokens.occupantTypeToken) { await this.eventService.setEventsOccupants({ event: body.event, userUUID: body.usersUUID }) }
else { throw new NotFoundException('User type not found') }
return body.event;
}
@Post('event/get')
async getEvent(@Body() body: eventGetValidator) {
const { typeToken, usersUUID } = body
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: typeToken } })
if (userType.type_token == tokens.employeeTypeToken) {
const allEvents = await this.eventService.getAllEventsEmployees(typeToken);
if (!allEvents) { throw new NotFoundException('Events not found') }
const selectedEvents = await this.eventService.getEventsEmployees(usersUUID);
const selectedEventsKeys = Object.values(selectedEvents || {}).map((value: any) => value.key) || [];
for (const event of allEvents) { if (selectedEventsKeys.includes(event.key)) { event.isSelected = true } else { event.isSelected = false } }
return { events: allEvents }
}
else if (userType.type_token == tokens.occupantTypeToken) {
const allEvents = await this.eventService.getAllEventsOccupants(typeToken);
if (!allEvents) { throw new NotFoundException('Events not found') }
const selectedEvents = await this.eventService.getEventsOccupants(usersUUID);
const selectedEventsKeys = Object.values(selectedEvents || {}).map((value: any) => value.key) || [];
for (const event of allEvents) { if (selectedEventsKeys.includes(event.key)) { event.isSelected = true } else { event.isSelected = false } }
return { events: allEvents }
} else { throw new NotFoundException('User type not found') }
}
@Post('page/set')
async setPage(@Body() body: { usersUUID: string, usersToken: string, url: string, page: Record<string, any> }) {
return await this.pagesService.setPageViaToken(body.usersUUID, body.usersToken, body.url, body.page)
}
@Post('page/get')
async getPage(@Body() body: { usersUUID: string, token: string, url?: string, skip?: number, limit?: number }) {
const pages = await this.pagesService.getPageViaToken(body.usersUUID, body.token, body.url, body.skip, body.limit)
return { pages }
}
@Post('page/configure')
async setPages(@Body() body: { chunkIndex: number; chunkCount: number; data: Record<string, any> }) {
return await this.pagesService.configurePages(body.data, body.chunkIndex, body.chunkCount)
}
}

View File

@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { MongoModule } from '@/src/database/mongo/mongo.module';
import { PrismaService } from '@/src/prisma.service';
import { MenusService } from '@/src/navigator/menus/menu.service';
import { NavigatorController } from '@/src/navigator/navigator.controller';
import { EventsService } from '@/src/navigator/events/events.service';
import { UrlHandler } from '@/src/utils/navigator/urlHandler';
import { PagesService } from '@/src/navigator/pages/pages.service';
@Module({
controllers: [NavigatorController],
imports: [MongoModule],
providers: [MenusService, EventsService, PagesService, PrismaService, UrlHandler],
exports: [MenusService, EventsService, PagesService, PrismaService]
})
export class NavigatorModule {
constructor() { }
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PagesService } from './pages.service';
describe('PagesService', () => {
let service: PagesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PagesService],
}).compile();
service = module.get<PagesService>(PagesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,172 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '@/src/prisma.service';
import { MongoService } from '@/src/database/mongo/mongo.service';
import { UrlHandler } from '@/src/utils/navigator/urlHandler';
@Injectable()
export class PagesService {
constructor(
private readonly prismaService: PrismaService,
private readonly mongoService: MongoService,
private readonly urlHandler: UrlHandler
) { }
tokens = { employeeTypeToken: "L9wBdwV9OlxsLAgh", occupantTypeToken: "j0adQOsJBR0xq24d" }
private async saveChunkToDB(data: Record<string, any>, chunkIndex: number) {
await this.mongoService.set("Pages");
if (chunkIndex == 1) { await this.mongoService.deleteMany({}) }
await this.mongoService.createMany(Object.values(data));
}
async getPagesOccupants(userUUID: string, usersToken: string) {
const user = await this.prismaService.users.findFirstOrThrow({ where: { uu_id: userUUID }, include: { people: true } });
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: usersToken } })
if (userType.type_token == this.tokens.occupantTypeToken) {
const person = user.people
const livingSpace = await this.prismaService.build_living_space.findFirstOrThrow({
where: { person_id: person.id, occupant_types: { user_types: { id: userType.id } } },
select: { build_parts_id: true }
})
const buildUUID = await this.prismaService.build.findFirstOrThrow({
where: { build_parts: { some: { id: livingSpace.build_parts_id } } }, select: { uu_id: true }
})
this.mongoService.set(`Pages/${buildUUID.uu_id}`);
console.log('usersToken', usersToken)
console.log('user.uu_id', user.uu_id)
const userPage = await this.mongoService.findOne({ [user.uu_id]: { $exists: true } })
console.log('userPage', userPage)
if (!userPage) { throw new NotFoundException('Users slot not found') }
const userPageSlot = userPage[user.uu_id][usersToken]
if (!userPageSlot) { throw new NotFoundException('Users slot not found') }
console.log('userPageSlot', userPageSlot)
const writeObject = {}
for (const [key, value] of Object.entries(userPageSlot)) {
writeObject[key] = `${key}:${value}`
}
console.log('writeObject', writeObject)
return writeObject
}
else { throw new NotFoundException('User type not found') }
}
async getPagesEmployee(userUUID: string, usersToken: string) {
const user = await this.prismaService.users.findFirstOrThrow({ where: { uu_id: userUUID }, include: { people: true } });
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: usersToken } })
if (userType.type_token == this.tokens.employeeTypeToken) {
const person = user.people[0]
const employee = await this.prismaService.employees.findFirstOrThrow({ where: { people_id: person.id, staff: { user_type_id: userType.id } } })
const companyUUID = await this.prismaService.companies.findFirstOrThrow({
where: { departments: { some: { duties: { some: { staff: { some: { uu_id: employee.staff_uu_id } } } } } } }, select: { uu_id: true }
})
this.mongoService.set(`Pages/${companyUUID.uu_id}`);
const userPage = await this.mongoService.findOne({ [user.uu_id]: { [usersToken]: { $exists: true } } })
if (!userPage) { throw new NotFoundException('Users slot not found') }
return userPage
}
else { throw new NotFoundException('User type not found') }
}
async setPageViaToken(userUUID: string, usersToken: string, url: string, page: Record<string, any>) {
const user = await this.prismaService.users.findFirstOrThrow({ where: { uu_id: userUUID }, include: { people: true } });
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: usersToken } })
const urlToken = await this.urlHandler.getSecureUrlToken(url)
if (userType.type_token == this.tokens.employeeTypeToken) {
const person = user.people[0]
const employee = await this.prismaService.employees.findFirstOrThrow({ where: { people_id: person.id, staff: { user_type_id: userType.id } } })
const companyUUID = await this.prismaService.companies.findFirstOrThrow({
where: { departments: { some: { duties: { some: { staff: { some: { uu_id: employee.staff_uu_id } } } } } } }, select: { uu_id: true }
})
this.mongoService.set(`Pages/${companyUUID.uu_id}`);
const userPage = await this.mongoService.findOne({ [employee.uu_id]: { $exists: true } });
if (!userPage) {
console.log('urlToken', urlToken)
} else { console.log('urlToken', urlToken) }
} else if (userType.type_token == this.tokens.occupantTypeToken) {
const person = user.people
const livingSpace = await this.prismaService.build_living_space.findFirstOrThrow({
where: { person_id: person.id, occupant_types: { user_types: { id: userType.id } } },
select: { uu_id: true, build_parts_id: true }
})
const buildUUID = await this.prismaService.build.findFirstOrThrow({
where: { build_parts: { some: { id: livingSpace.build_parts_id } } }, select: { uu_id: true }
})
this.mongoService.set(`Pages/${buildUUID.uu_id}`);
const userPage = await this.mongoService.findOne({ [user.uu_id]: { $exists: true } });
if (!userPage) {
const newUserPageSlot = await this.mongoService.create({ [user.uu_id]: { [`${usersToken}`]: { [`${urlToken}`]: `${page.key}` } } })
return newUserPageSlot
} else {
const updatedUserPageSlot = await this.mongoService.updateOne(userPage._id, { [`${user.uu_id}.${usersToken}.${urlToken}`]: `${page.key}` })
return updatedUserPageSlot ? { status: "success", data: updatedUserPageSlot } : { status: "error", data: null }
}
// console.log('urlToken', { [user.uu_id]: { [`${body.usersToken}`]: { [`${urlToken}`]: `${body.page.key}` } } })
}
else { throw new NotFoundException('User type not found') }
}
async getPageViaToken(usersUUID: string, token: string, url?: string, skip?: number, limit?: number) {
this.mongoService.set("Pages");
const addUrlQuery = url ? { url: url } : {};
const user = await this.prismaService.users.findFirstOrThrow({ where: { uu_id: usersUUID }, include: { people: true } });
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: token } })
if (userType.type_token == this.tokens.employeeTypeToken) {
const person = user.people[0]
const pages = await this.mongoService.findMany({
$and: [
{ $or: [{ includeTokens: { $in: ['*'] } }, { includeTokens: { $in: [token] } }] },
{ $nor: [{ excludeTokens: { $in: ['*'] } }, { excludeTokens: { $in: [token] } }] },
addUrlQuery,
{ typeToken: this.tokens.employeeTypeToken },
],
}, limit || 50, skip || 0, ['url'], ['asc'])
if (!pages) { throw new NotFoundException(`Pages not found. User type: ${userType.type_token}`) }
const employee = await this.prismaService.employees.findFirstOrThrow({
where: { people_id: person.id, staff: { user_type_id: userType.id } },
select: { uu_id: true, staff_uu_id: true }
})
const companyUUID = await this.prismaService.companies.findFirstOrThrow({
where: { departments: { some: { duties: { some: { staff: { some: { uu_id: employee.staff_uu_id } } } } } } }, select: { uu_id: true }
})
this.mongoService.set(`Pages/${companyUUID.uu_id}`);
const usersPages = await this.mongoService.findMany({ [employee.uu_id]: { $exists: true } });
return pages;
}
else if (userType.type_token == this.tokens.occupantTypeToken) {
const person = user.people
const pages = await this.mongoService.findMany({
$and: [
{ $or: [{ includeTokens: { $in: ['*'] } }, { includeTokens: { $in: [token] } }] },
{ $nor: [{ excludeTokens: { $in: ['*'] } }, { excludeTokens: { $in: [token] } }] },
addUrlQuery,
{ typeToken: this.tokens.occupantTypeToken },
],
}, limit || 50, skip || 0, ['url'], ['asc'])
console.log('pages', pages)
if (!pages) { throw new NotFoundException(`Pages not found. User type: ${userType.type_token}`) }
const livingSpace = await this.prismaService.build_living_space.findFirstOrThrow({
where: { person_id: person.id, occupant_types: { user_types: { id: userType.id } } },
select: { uu_id: true, build_parts_id: true }
})
console.log('livingSpace', livingSpace)
const buildUUID = await this.prismaService.build.findFirstOrThrow({
where: { build_parts: { some: { id: livingSpace.build_parts_id } } }, select: { uu_id: true }
})
this.mongoService.set(`Pages/${buildUUID.uu_id}`);
const usersPages = await this.mongoService.findMany({ [livingSpace.uu_id]: { $exists: true } });
console.log('usersPages', usersPages)
return Object.entries(pages).map(([key, value]: [string, any]) => {
if (usersPages.some((page: any) => page[key])) { value.isSelected = true } else { value.isSelected = false }
return value;
})
}
else { throw new NotFoundException('User type not found') }
}
async configurePages(data: Record<string, any>, chunkIndex: number, chunkCount: number) {
const count = Object.keys(data).length;
console.log(`🧩 Chunk [${chunkIndex}/${chunkCount}] alındı. Kayıt sayısı: ${count}`);
await this.saveChunkToDB(data, chunkIndex);
return { message: 'Chunk işlendi', count };
}
}

View File

@@ -1,4 +1,4 @@
import { z } from 'zod';
import { uuid, z } from 'zod';
// ENUM
export const UserType = {
@@ -9,8 +9,8 @@ export type UserType = (typeof UserType)[keyof typeof UserType];
// Credentials
export const CredentialsSchema = z.object({
person_id: z.number(),
person_name: z.string(),
uuid: z.string(),
fullName: z.string(),
});
export type Credentials = z.infer<typeof CredentialsSchema>;
@@ -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(),
@@ -42,7 +42,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(),
@@ -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(),
@@ -76,7 +77,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(),
@@ -85,38 +86,194 @@ export const AuthTokenSchema = z.object({
default_language: z.string(),
}),
credentials: CredentialsSchema,
selectionList: z
.object({
type: z.string(),
list: z.array(z.any()).optional().default([]),
})
.optional()
.default({
type: '',
list: [],
}),
});
export type AuthToken = z.infer<typeof AuthTokenSchema>;
export const EmployeeTokenSchema = z.object({
uuid: z.string(),
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(),
events: z.record(z.string(), z.string()).nullable(),
selection: z.record(z.string(), z.unknown()).nullable(),
typeToken: z.string(),
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(),
uuid: 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({
key: z.string(),
icon: z.string(),
text: z.object({ tr: z.string(), en: z.string() }),
page: z.string().nullable(),
token: z.string().nullable(),
color: z.string(),
subs: z.array(z.lazy(() => OccupantTokenSchema.shape.menu.element)).nullable()
})).nullable(),
pages: z.record(z.string(), z.string()).nullable(),
events: z.record(z.string(), z.string()).nullable(),
selection: z.record(z.string(), z.unknown()).nullable(),
typeToken: z.string(),
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', [
EmployeeTokenSchema,
OccupantTokenSchema,
]);
export const TokenDictTypes = z.discriminatedUnion('kind', [EmployeeTokenSchema, OccupantTokenSchema]);
export type TokenDictInterface = z.infer<typeof TokenDictTypes>;

View File

@@ -2,13 +2,14 @@ import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { PrismaModule } from '@/prisma/prisma.module';
import { CacheService } from '../cache.service';
import { CacheService } from '../database/redis/redis.service';
import { UtilsModule } from '../utils/utils.module';
import { RedisModule } from '../database/redis/redis.module';
@Module({
imports: [PrismaModule, UtilsModule],
providers: [UsersService, CacheService],
imports: [PrismaModule, UtilsModule, RedisModule],
providers: [UsersService],
controllers: [UsersController],
exports: [UsersService],
})
export class UsersModule {}
export class UsersModule { }

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/src/prisma.service';
import { Prisma, users } from '@prisma/client';
import { CacheService } from '../cache.service';
import { CacheService } from '../database/redis/redis.service';
import { PaginationHelper, PaginationInfo } from '../utils/pagination-helper';
@Injectable()
@@ -10,7 +10,7 @@ export class UsersService {
private prisma: PrismaService,
private cacheService: CacheService,
private paginationHelper: PaginationHelper,
) {}
) { }
async findAll(filter: any): Promise<Partial<users>[]> {
return this.prisma.users.findMany({

View File

@@ -0,0 +1,63 @@
import { ForbiddenException, MisdirectedException, Injectable } from "@nestjs/common";
import { RedisHandlers } from "../store/redisHandlers";
@Injectable()
export class Navigator {
constructor(private redisHandler: RedisHandlers) { }
async getAllInfos(mainService: any) {
const mainServiceMapper = mainService?.mapper
if (!mainServiceMapper) { throw new ForbiddenException(`Mapper in ${mainService.constructor.name} is missing or null`) }
const allInfos = Object.entries(mainServiceMapper).map(([key, value]) => {
return {
key,
value
}
})
return allInfos
}
async getInfos(mainService: any, userToken: string) {
// Get asked service by userToken
const mainServiceMapper = mainService?.mapper
if (!mainServiceMapper) { throw new ForbiddenException(`Mapper in ${mainService.constructor.name} is missing or null`) }
// Get related events from mainServiceMapper by userToken
const relatedService = mainServiceMapper?.[userToken]
if (!relatedService) { throw new MisdirectedException(`No service found for drive token: ${userToken}`) }
// Call event infos from relatedService
return await relatedService.infoEvents(userToken);
}
async getService(request: any, mapper: any) {
// Get request drive token from acess control guard and retrieve related Service
const driveToken = request.driveToken
if (!driveToken) { throw new Error('Drive token is missing or null') }
// Get second part of drive token which is user type token
const secondPartOfDriveToken = driveToken.split(":")[1]
if (!secondPartOfDriveToken) { throw new Error('Drive token is missing or null') }
// Get related service from mapper which function maps registered events to functions
return mapper[secondPartOfDriveToken];
}
async getFunction(request: any, mapper: any, query: any) {
const relatedService = await this.getService(request, mapper)
if (!relatedService) { throw new Error(`No service found for drive token: ${request.driveToken}`) }
try {
// Get function mapper from related service
if (!relatedService.mapper) { throw new Error(`Mapper in ${relatedService.constructor.name} is missing or null`) }
// Get redis select token object from redis
const selectObject = await this.redisHandler.getSelectFromRedis(request);
if (!selectObject) { throw new Error(`Select object is missing or null`) }
if (!selectObject.value.events) { throw new Error(`Events in select object is missing or null`) }
const eventKey = Object.entries(selectObject.value.events).filter((key) => key.includes(request.driveToken))[0]
if (!eventKey) { throw new Error(`No event is registered for this user ${request.driveToken}`) }
// Get function to call from related service mapper
const functionToCall = relatedService.mapper[eventKey.join(":")];
if (!functionToCall || typeof functionToCall !== 'function') {
throw new Error(`No function found for drive token: ${request.driveToken}`);
}
return await functionToCall(query);
} catch (error) { throw new ForbiddenException(`This user is not allowed to access this endpoint. Please contact your system administrator.`) }
}
}

View File

@@ -0,0 +1,67 @@
import { Injectable } from "@nestjs/common";
import { Events, Mapper } from "@/src/utils/types/url";
import { createHash } from 'crypto';
@Injectable()
export class UrlHandler {
private createSecureKeyWithoutLib(url: string): string {
const subString = createHash('sha256').update(url).digest().toString('base64').substring(0, 48)
return subString.replace(/=/g, 'E').replace(/-/g, 'M').replace(/_/g, 'N').replace(/\+/g, 'P').replace(/\//g, 'Q')
}
async getSecureUrlToken(url: string): Promise<string> {
return this.createSecureKeyWithoutLib(url);
}
async getEvents(events: Events, mapper: Mapper) {
for (const keyUrl of Object.keys(mapper)) {
const splittedMapper = keyUrl.split(':')
const eToken = splittedMapper[0]
const token = splittedMapper[1]
const key = splittedMapper[2]
const eventKey = `${eToken}:${token}`
if (Object.keys(events).includes(eventKey)) {
// Check if the event contains an item with the matching key
const eventArray = events[eventKey]
const foundEvent = eventArray.find(item => item.key === key)
if (!foundEvent) {
throw new Error(`Event key ${key} not found in event ${eventKey}`)
}
} else {
throw new Error(`Event ${eventKey} not found in events`)
}
}
return mapper;
}
async infoEvents(events: Events, urlRetriever: string | null = null, functionRetriever: string | null = null) {
if (urlRetriever && !functionRetriever) {
console.log("urlRetriever", urlRetriever)
if (events[urlRetriever]) {
return [[urlRetriever, events[urlRetriever]]];
}
return [];
} else if (urlRetriever && functionRetriever) {
if (events[urlRetriever]) {
const eventItem = events[urlRetriever].find(item => item.key === functionRetriever);
if (eventItem) {
return [[urlRetriever, { [functionRetriever]: eventItem }]];
}
}
return [];
} else if (!urlRetriever && functionRetriever) {
const filteredEvents: [string, any][] = [];
Object.entries(events).forEach(([url, eventArray]) => {
const eventItem = eventArray.find(item => item.key === functionRetriever);
if (eventItem) {
filteredEvents.push([url, { [functionRetriever]: eventItem }]);
}
});
return filteredEvents;
} else {
return Object.entries(events);
}
}
}

View File

@@ -18,7 +18,7 @@ type ModelDelegate = {
@Injectable()
export class PaginationHelper {
constructor(private prisma: PrismaService) {}
constructor(private prisma: PrismaService) { }
/**
* Sayfalama destekli sorgu yapar
@@ -55,4 +55,11 @@ export class PaginationHelper {
},
};
}
async findWithPagination(
query: any & { page?: number; pageSize?: number },
service: ModelDelegate,
): Promise<{ data: any[]; pagination: PaginationInfo }> {
return this.paginate(service, query);
}
}

View File

@@ -0,0 +1,6 @@
import { Injectable } from "@nestjs/common";
@Injectable()
export class MongoHandler {
constructor() { }
}

View File

@@ -2,11 +2,10 @@ import {
TokenDictTypes,
TokenDictInterface,
AuthToken,
UserType,
} from '@/src/types/auth/token';
import { CacheService } from '@/src/cache.service';
import { users } from '@prisma/client';
import { PasswordHandlers } from './login_handler';
AuthTokenSchema,
} from '../../types/auth/token';
import { CacheService } from '../../database/redis/redis.service';
import { PasswordHandlers } from './loginHandler';
import { Injectable, ForbiddenException } from '@nestjs/common';
interface LoginFromRedis {
@@ -22,20 +21,21 @@ interface SelectFromRedis {
@Injectable()
export class RedisHandlers {
AUTH_TOKEN = 'AUTH_TOKEN';
SELECT_TOKEN = 'SELECT_TOKEN';
constructor(
private readonly cacheService: CacheService,
private readonly passwordService: PasswordHandlers,
) {}
) { }
/**
* Validates that a Redis key follows the expected format
* 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,20 @@ 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;
}
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;
}
@@ -79,8 +91,8 @@ export class RedisHandlers {
return this.passwordService.generateAccessToken();
}
private async scanKeys(pattern: string): Promise<string[]> {
this.validateRedisKey(pattern);
private async scanKeys(pattern: string, type: string): Promise<string[]> {
this.validateRedisKey(pattern, type);
const client = (this.cacheService as any).client;
if (!client) throw new Error('Redis client not available');
@@ -103,7 +115,7 @@ export class RedisHandlers {
async getLoginFromRedis(req: Request): Promise<LoginFromRedis | null> {
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');
}
@@ -113,7 +125,7 @@ export class RedisHandlers {
if (parts[1] === parts[2]) {
const value = await this.cacheService.get(key);
if (value) {
return { key, value };
return { key: value.key, value: value.value };
}
}
}
@@ -122,13 +134,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<SelectFromRedis | null> {
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(
@@ -138,7 +152,7 @@ export class RedisHandlers {
for (const key of keys) {
const value = await this.cacheService.get(key);
if (value) {
return { key, value };
return { key: value.key, value: value.value };
}
}
throw new ForbiddenException(
@@ -147,7 +161,56 @@ 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 callExistingLoginToken(userUUID: string): Promise<string | null> {
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<string | null> {
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<any> {
@@ -161,15 +224,15 @@ export class RedisHandlers {
}
async renewTtlLoginFromRedis(req: Request): Promise<any> {
const mergedKey = this.mergeLoginKey(req);
const value = await this.cacheService.get(mergedKey);
return this.cacheService.set_with_ttl(mergedKey, value, 86400);
const loginToken = await this.getLoginFromRedis(req);
if (!loginToken) { throw new ForbiddenException('Login token not found') }
return this.cacheService.set_with_ttl(loginToken.key, loginToken.value, 60 * 30);
}
async renewTtlSelectFromRedis(req: Request): Promise<any> {
const mergedKey = this.mergeSelectKey(req);
const value = await this.cacheService.get(mergedKey);
return this.cacheService.set_with_ttl(mergedKey, value, 60 * 30);
const selectToken = await this.getSelectFromRedis(req);
if (!selectToken) { throw new ForbiddenException('Select token not found') }
return this.cacheService.set_with_ttl(selectToken.key, selectToken.value, 60 * 30);
}
async setLoginToRedis(token: AuthToken, userUUID: string): Promise<any> {
@@ -186,7 +249,7 @@ export class RedisHandlers {
livingUUID: string,
): Promise<any> {
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;
}

View File

@@ -0,0 +1,18 @@
export interface interfaceMenu {
key: string;
icon: string;
text: { tr: string, en: string };
page: string | null;
token: string | null;
color: string;
subs: interfaceMenu[] | null;
}
export interface interfaceMapper {
[key: string]: string;
}
export interface interfaceMenus {
Menu: interfaceMenu[];
Mapper: interfaceMapper;
}

View File

@@ -0,0 +1,16 @@
export interface Events {
[key: string]: Array<{
endpoint: string;
eToken: string;
token: string;
key: string;
description: string;
isDefault: boolean;
query: Record<string, boolean>;
pages: string[];
}>;
}
export interface Mapper {
[key: string]: (query: any) => any;
}

View File

@@ -1,18 +1,18 @@
import { Module } from '@nestjs/common';
import { PaginationHelper } from './pagination-helper';
import { PrismaService } from '@/src/prisma.service';
import { RedisHandlers } from './auth/redis_handlers';
import { PasswordHandlers } from './auth/login_handler';
import { CacheService } from '@/src/cache.service';
import { RedisHandlers } from './store/redisHandlers';
import { PasswordHandlers } from './store/loginHandler';
import { RedisModule } from '../database/redis/redis.module';
@Module({
imports: [RedisModule],
providers: [
PaginationHelper,
PrismaService,
RedisHandlers,
PasswordHandlers,
CacheService,
],
exports: [PaginationHelper, RedisHandlers, PasswordHandlers, CacheService],
exports: [PaginationHelper, RedisHandlers, PasswordHandlers],
})
export class UtilsModule {}
export class UtilsModule { }

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"types": ["node"],
"types": ["node", "jest"],
"typeRoots": ["./node_modules/@types"],
"module": "commonjs",
"declaration": true,
@@ -19,7 +19,6 @@
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false,
// "baseUrl": "./src",
"paths": {
"@/*": ["*"]
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -9,17 +9,24 @@
"version": "0.1.0",
"dependencies": {
"clsx": "^2.1.1",
"cookies-next": "^6.1.0",
"glob": "^11.0.3",
"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"
"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 +545,47 @@
"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/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -1045,6 +1093,28 @@
"@types/react": "^19.0.0"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
@@ -1090,6 +1160,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",
@@ -1109,7 +1188,6 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -1121,8 +1199,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
@@ -1135,6 +1212,41 @@
"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/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -1142,12 +1254,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",
@@ -1158,6 +1306,16 @@
"node": ">=8"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/enhanced-resolve": {
"version": "5.18.2",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
@@ -1172,6 +1330,43 @@
"node": ">=10.13.0"
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.0.3",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1191,6 +1386,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",
@@ -1198,6 +1417,33 @@
"license": "MIT",
"optional": true
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/jackspeak": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/jiti": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
@@ -1447,6 +1693,35 @@
"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/lru-cache": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
"engines": {
"node": "20 || >=22"
}
},
"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",
@@ -1457,11 +1732,24 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -1496,6 +1784,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",
@@ -1575,6 +1869,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",
@@ -1630,6 +1930,34 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1686,6 +2014,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",
@@ -1748,6 +2097,36 @@
"@img/sharp-win32-x64": "0.34.3"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -1767,6 +2146,100 @@
"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/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@@ -1866,6 +2339,104 @@
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
@@ -1875,6 +2446,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"
}
}
}
}

View File

@@ -6,21 +6,29 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"reset-sync": "rm ./.next/server/app/[locale]/sync.lock"
},
"dependencies": {
"clsx": "^2.1.1",
"cookies-next": "^6.1.0",
"glob": "^11.0.3",
"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"
"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"
}
}

View File

@@ -0,0 +1,193 @@
'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 } from "lucide-react";
import { apiPostFetcher } from '@/lib/fetcher';
import { useRouter } from '@/i18n/routing';
export default function LoginPage() {
const t = useTranslations('Login');
const loginSchema = z.object({
accessKey: z.string().email(t('emailWrong')),
password: z.string().min(6, t('passwordWrong')),
rememberMe: z.boolean().default(false),
});
type LoginInterface = z.infer<typeof loginSchema>;
interface LoginFormErrors {
accessKey?: boolean;
password?: boolean;
rememberMe?: boolean;
}
const [errors, setErrors] = useState<LoginFormErrors>({});
const [showPassword, setShowPassword] = useState(false);
const router = useRouter();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
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 (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-cyan-50 flex items-center justify-center p-2 sm:p-4">
<div className="w-full max-w-7xl mx-auto">
<div className="flex flex-col lg:flex-row gap-4 sm:gap-6 md:gap-8 items-center justify-center w-full min-h-[60vh] md:min-h-[70vh] lg:min-h-[80vh]">
{/* Left side - Login form (now takes full width) */}
<div className="card bg-white/90 backdrop-blur-sm shadow-xl border border-indigo-100 rounded-2xl w-full overflow-auto">
<div className="card-body p-4 sm:p-6 md:p-8 lg:p-10 xl:p-12">
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-5 md:space-y-6 max-w-md mx-auto w-full">
<div className="text-center mb-4 sm:mb-6 md:mb-8">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-800 mb-1 sm:mb-2">{t('welcomeBack')}</h1>
<p className="text-sm sm:text-base text-gray-600">{t('continueJourney')}</p>
</div>
<div className="form-control w-full">
<label className="label p-0 mb-1 sm:mb-1.5 md:mb-2">
<span className="label-text font-medium text-gray-700 text-sm sm:text-base">{t('email')}</span>
</label>
<div className="relative">
<input
type="text"
name="email"
className={`input input-bordered rounded-2xl text-black h-14 bg-white w-full pl-8 sm:pl-10 md:pl-12 py-3 sm:py-4 transition-all
duration-300 border-gray-200 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 ${errors.accessKey ? 'border-red-500 focus:border-red-500 focus:ring-red-200' : ''}`}
placeholder={t('email')}
// style={{
// WebkitBackgroundClip: 'text',
// WebkitTextFillColor: 'black',
// }}
/>
<Mail className="absolute left-2 sm:left-3 md:left-4 top-1/2 transform -translate-y-1/2 w-3 h-3 sm:w-4 sm:h-4 md:w-5 md:h-5 text-gray-400 pointer-events-none z-10" />
</div>
{errors.accessKey && (
<div className="label p-0 pt-1">
<span className="label-text-alt text-red-500 flex items-center gap-1 text-xs sm:text-sm">
<svg className="w-2.5 h-2.5 sm:w-3 sm:h-3 md:w-4 md:h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
{t('emailWrong')}
</span>
</div>
)}
</div>
<div className="form-control w-full">
<label className="label p-0 mb-1 sm:mb-1.5 md:mb-2">
<span className="label-text font-medium text-gray-700 text-sm sm:text-base">{t('password')}</span>
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
name="password"
className={`input input-bordered rounded-2xl text-black h-14 bg-white w-full pl-8 sm:pl-10 md:pl-12 pr-8 sm:pr-10 md:pr-12 py-3 sm:py-4 transition-all duration-300 border-gray-200 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 ${errors.password ? 'border-red-500 focus:border-red-500 focus:ring-red-200' : ''}`}
placeholder="••••••••"
// style={{
// WebkitBackgroundClip: 'text',
// WebkitTextFillColor: 'black',
// }}
/>
<Lock className="absolute left-2 sm:left-3 md:left-4 top-1/2 transform -translate-y-1/2 w-3 h-3 sm:w-4 sm:h-4 md:w-5 md:h-5 text-gray-400 pointer-events-none z-10" />
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-2 sm:right-3 md:right-4 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors z-10"
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? <EyeOff size={14} className="sm:w-4 sm:h-4 md:w-5 md:h-5" /> : <Eye size={14} className="sm:w-4 sm:h-4 md:w-5 md:h-5" />}
</button>
</div>
{errors.password && (
<div className="label p-0 pt-1">
<span className="label-text-alt text-red-500 flex items-center gap-1 text-xs sm:text-sm">
<svg className="w-2.5 h-2.5 sm:w-3 sm:h-3 md:w-4 md:h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
{t('passwordWrong')}
</span>
</div>
)}
</div>
<div className="flex items-center justify-between flex-wrap gap-2">
<label className="label cursor-pointer flex items-center gap-1.5 sm:gap-2 p-0">
<input name="rememberMe" type="checkbox" className="checkbox checkbox-primary checkbox-sm [--chkbg:theme(colors.indigo.500)] [--chkfg:theme(colors.white)] border-indigo-300" />
<span className="label-text text-gray-700 text-xs sm:text-sm">{t('rememberMe')}</span>
</label>
<Link href="/forgot-password" className="text-indigo-600 hover:text-indigo-800 text-xs sm:text-sm font-medium transition-colors">
{t('forgotPassword')}
</Link>
</div>
<button
type="submit"
className="btn bg-indigo-600 hover:bg-indigo-700 border-0 w-full mt-2 sm:mt-3 md:mt-4 py-2 sm:py-2.5 md:py-3 shadow-md hover:shadow-lg transition-all duration-300 text-white font-medium text-sm sm:text-base"
>
{t('login')}
</button>
<div className="divider my-3 sm:my-4 md:my-6 text-gray-400 before:bg-gray-200 after:bg-gray-200 text-xs sm:text-sm">{t('orContinueWith')}</div>
<div className="grid grid-cols gap-3 sm:gap-3 md:gap-6">
<button
type="button"
className="btn btn-outline border-gray-300 text-gray-700 hover:bg-gray-50 py-2 sm:py-2.5 md:py-3 flex items-center justify-center gap-1.5 sm:gap-2 text-xs sm:text-sm"
>
<svg className="w-3 h-3 sm:w-4 sm:h-4 md:w-5 md:h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
</svg>
Google
</button>
<button
type="button"
className="btn btn-outline border-gray-300 text-gray-700 hover:bg-gray-50 py-2 sm:py-2.5 md:py-3 flex items-center justify-center gap-1.5 sm:gap-2 text-xs sm:text-sm"
>
<svg className="w-3 h-3 sm:w-4 sm:h-4 md:w-5 md:h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M24 12.073c0-5.8-4.702-10.5-10.5-10.5s-10.5 4.7-10.5 10.5c0 5.24 3.84 9.584 8.86 10.373v-7.337h-2.666v-3.037h2.666V9.458c0-2.63 1.568-4.085 3.966-4.085 1.15 0 2.35.205 2.35.205v2.584h-1.322c-1.304 0-1.71.81-1.71 1.64v1.97h2.912l-.465 3.036H15.14v7.337c5.02-.788 8.859-5.131 8.859-10.372z" fill="#1877F2" />
</svg>
Facebook
</button>
</div>
</form>
</div>
</div>
</div>
<div className="text-center mt-4 sm:mt-6 md:mt-8 text-gray-500 text-xs sm:text-sm">
<p>{t('copyright')}</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,77 @@
"use client";
export default function FromFigma() {
return (
<div className="min-h-screen bg-[#f6f6f6] flex flex-col">
<div className="flex-1 flex justify-center items-center">
<div className="w-full max-w-6xl bg-white rounded-xl shadow-none flex flex-col md:flex-row gap-12 p-12 mx-4">
{/* Left: Recent logins */}
<div className="flex-1 flex flex-col items-start justify-center">
<div className="w-14 h-14 rounded-full bg-gray-300 mb-8" />
<h2 className="text-3xl font-semibold text-gray-800 mb-2">Recent logins</h2>
<div className="text-gray-500 text-sm mb-7">Click your picture or add an account</div>
<div className="flex gap-5">
{/* User card */}
<div className="flex flex-col items-center bg-white rounded-lg border border-gray-200 w-40 shadow-sm">
<div className="relative w-full aspect-square rounded-t-lg overflow-hidden">
<img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=256&q=80" alt="Mika Lee" className="w-full h-full object-cover" />
<button className="absolute top-2 left-2 w-4 h-4 bg-white rounded-full flex items-center justify-center text-xs text-gray-400">×</button>
</div>
<div className="w-full text-center text-sm text-gray-700 py-2 border-t border-gray-200">Mika Lee</div>
</div>
{/* Add account card */}
<div className="flex flex-col items-center bg-white rounded-lg border border-gray-200 w-40 shadow-sm cursor-pointer">
<div className="flex flex-col justify-center items-center w-full aspect-square rounded-t-lg">
<span className="text-3xl text-gray-400"></span>
</div>
<div className="w-full text-center text-sm text-gray-500 py-2 border-t border-gray-200">Add an account</div>
</div>
</div>
</div>
{/* Right: Login form */}
<div className="flex-1 flex flex-col justify-center">
<div className="bg-white rounded-xl border border-gray-200 px-8 py-8 shadow-none">
<form className="space-y-6">
<div>
<label className="block text-sm text-gray-500 mb-1">Your email</label>
<input type="email" className="w-full border border-gray-300 rounded-md px-4 py-3 text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-gray-200" />
</div>
<div>
<div className="flex items-center justify-between mb-1">
<label className="block text-sm text-gray-500">Your password</label>
<div className="flex items-center gap-1 text-gray-400 text-sm cursor-pointer select-none">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M13.875 18.825A10.05 10.05 0 0 1 12 19c-5.523 0-10-4.477-10-10 0-1.657.403-3.22 1.125-4.575m1.35-2.025A9.959 9.959 0 0 1 12 3c5.523 0 10 4.477 10 10 0 1.657-.403 3.22-1.125 4.575" /></svg>
Hide
</div>
</div>
<input type="password" className="w-full border border-gray-300 rounded-md px-4 py-3 text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-gray-200" />
</div>
<button type="button" className="w-full bg-gray-300 text-gray-500 rounded-full py-3 text-lg font-medium cursor-not-allowed" disabled>Log in</button>
<div className="text-center mt-2">
<a href="#" className="text-gray-600 underline text-sm">Forget your password?</a>
</div>
</form>
</div>
<button className="w-full border border-gray-400 rounded-full py-3 mt-8 text-lg font-medium text-gray-700 bg-white hover:bg-gray-50 transition">Create an account</button>
</div>
</div>
</div>
{/* Footer */}
<footer className="w-full border-t border-gray-200 bg-white py-4 px-8 flex flex-wrap items-center justify-between text-xs text-gray-500 gap-2">
<div className="flex flex-wrap gap-6">
<a href="#" className="hover:underline">Sign up</a>
<a href="#" className="hover:underline">Log in</a>
<a href="#" className="hover:underline">Help Center</a>
<a href="#" className="hover:underline">Terms of Service</a>
<a href="#" className="hover:underline">Privacy Policy</a>
<a href="#" className="hover:underline">About</a>
<a href="#" className="hover:underline">Settings</a>
</div>
<div className="flex items-center gap-2">
English (united States)
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg>
</div>
</footer>
</div>
);
}

View File

@@ -0,0 +1,18 @@
'use server';
import LocaleSwitcherServer from '@/components/LocaleSwitcherServer';
import LoginPage from './LoginPage';
import { Locale } from 'next-intl';
import { checkAccessOnLoginPage } from '@/app/api/guards';
export default async function PageLogin({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
await checkAccessOnLoginPage(locale as Locale);
return (
<div>
<div className='absolute top-2 right-2'>
<LocaleSwitcherServer locale={locale} pathname="/login" />
</div>
<LoginPage />
</div>
);
}

View File

@@ -0,0 +1,413 @@
'use client';
import { useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { useRouter } from '@/i18n/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<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
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);
}
};
useEffect(() => { fetchSelectionList() }, []);
const handleSelection = (uuid: string) => { setSelectedOption(uuid) };
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');
if (selectionList?.type === 'employee') { router.push('/office') } else if (selectionList?.type === 'occupant') { 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) }
};
if (selectionList) {
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-cyan-50 p-4 sm:p-6 md:p-8">
<div className="max-w-6xl mx-auto w-full h-full flex flex-col">
<div className="text-center mb-8 sm:mb-10 mt-4 sm:mt-6">
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-800 mb-2">{t('title')}</h1>
<p className="text-base sm:text-lg text-gray-600">{t('description')}</p>
</div>
<div className="flex-grow flex flex-col">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 flex-grow">
{selectionList?.list?.map((item: any) => {
if (selectionList.type === 'employee') {
const staff = item.staff;
const department = staff?.duties?.departments;
const company = department?.companies;
return (
<div
key={item.uu_id}
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
onClick={() => handleSelection(item.uu_id)}
>
<div>
<div className="flex items-center mb-4">
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
{staff?.staff_code?.charAt(0) || 'E'}
</div>
<h3 className="text-lg font-bold text-gray-800">{t('staff')}: {staff?.staff_code || t('employee')}</h3>
</div>
<div className="space-y-2">
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span className="font-medium text-xs text-gray-700">{t('uuid')}:</span>
<span className="ml-2 font-mono text-xs text-gray-600">{item?.uu_id}</span>
</div>
<div className="pt-2 border-t border-gray-100 mt-2">
<div className="flex items-center mb-1">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
<span className="font-medium text-gray-700">{t('department')}</span>
</div>
<div className="ml-6 mt-1 space-y-1">
<div className="flex items-center">
<span className="text-xs text-gray-500 w-16">{t('name')}:</span>
<span className="text-sm text-gray-600">{department?.department_name || 'N/A'}</span>
</div>
<div className="flex items-center">
<span className="text-xs text-gray-500 w-16">{t('code')}:</span>
<span className="text-sm text-gray-600">{department?.department_code || 'N/A'}</span>
</div>
</div>
</div>
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 14v3m4-3v3m4-3v3M3 21h18M3 10h18M3 7l9-4 9 4M4 10h16v11H4V10z"></path>
</svg>
<span className="font-medium text-gray-700">{t('company')}:</span>
<span className="ml-2 text-sm text-gray-600">{company?.public_name || company?.formal_name || 'N/A'}</span>
</div>
</div>
</div>
{selectedOption === item.uu_id && (
<div className="mt-4 flex justify-end">
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
)}
</div>
);
}
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 (
<div
key={item.uu_id}
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
onClick={() => handleSelection(item.uu_id)}
>
<div>
<div className="flex items-center mb-4">
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
{occupantType?.occupant_code?.charAt(0) || 'O'}
</div>
<h3 className="text-lg font-bold text-gray-800">{t('occupant_type')}: {occupantType?.occupant_type}</h3>
</div>
<div className="space-y-2">
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span className="font-medium text-xs text-gray-700">{t('uuid')}:</span>
<span className="ml-2 font-mono text-xs text-gray-600">{item?.uu_id}</span>
</div>
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span className="font-medium text-gray-700">{t('occupant_code')}:</span>
<span className="ml-2 font-semibold text-indigo-600">{occupantType?.occupant_code}</span>
</div>
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
<span className="font-medium text-gray-700">{t('building')}:</span>
<span className="ml-2 text-gray-600">{build?.build_name || 'Building'}</span>
</div>
<div className="flex items-center">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
<span className="font-medium text-gray-700">{t('type')}:</span>
<span className="ml-2 text-gray-600">{enums?.value}</span>
</div>
<div className="pt-2 border-t border-gray-100 mt-2">
<div className="flex items-center mb-1">
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
<span className="font-medium text-gray-700">{t('part_details')}</span>
</div>
<div className="grid grid-cols-2 gap-2 ml-6 mt-1">
<div className="flex items-center">
<span className="text-xs text-gray-500 w-12">{t('code')}:</span>
<span className="text-sm text-gray-600">{buildPart?.part_code}</span>
</div>
<div className="flex items-center">
<span className="text-xs text-gray-500 w-12">{t('no')}:</span>
<span className="text-sm text-gray-600">{buildPart?.part_no}</span>
</div>
<div className="flex items-center">
<span className="text-xs text-gray-500 w-12">{t('level')}:</span>
<span className="text-sm text-gray-600">{buildPart?.part_level}</span>
</div>
<div className="flex items-center">
<span className="text-xs text-gray-500 w-12">{t('status')}:</span>
<span className={`text-sm font-medium ${buildPart?.human_livable ? 'text-green-600' : 'text-red-600'}`}>
{buildPart?.human_livable ? t('livable') : t('not_livable')}
</span>
</div>
</div>
</div>
</div>
</div>
{selectedOption === item.uu_id && (
<div className="mt-4 flex justify-end">
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
)}
</div>
);
}
return (
<div
key={item.uu_id}
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
onClick={() => handleSelection(item.uu_id)}
>
<div>
<div className="flex items-center mb-4">
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
{item.uu_id?.charAt(0) || 'S'}
</div>
<h3 className="text-lg font-bold text-gray-800">{selectionList.type || t('selection')}</h3>
</div>
<p className="text-gray-600 text-sm">{item.uu_id || t('id')}</p>
</div>
{selectedOption === item.uu_id && (
<div className="mt-4 flex justify-end">
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
)}
</div>
);
})}
</div>
<div className="mt-8 sm:mt-10 flex justify-center">
<button
className={`px-8 py-4 rounded-xl font-bold text-white transition-all duration-300 text-lg ${selectedOption
? 'bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 shadow-lg shadow-indigo-200 hover:shadow-indigo-300 transform hover:scale-105'
: 'bg-gray-400 cursor-not-allowed'}`}
disabled={!selectedOption || isLoading}
onClick={handleContinue}
>
{isLoading ? t('processing') : selectedOption ? t('continue') : t('select_option')}
</button>
</div>
</div>
</div>
</div>
);
} else {
return (
<>
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-cyan-50 p-4 sm:p-6 md:p-8">
<div className="max-w-6xl mx-auto w-full h-full flex flex-col">
<div className="text-center mb-8 sm:mb-10 mt-4 sm:mt-6">
<div className="skeleton h-8 w-64 mx-auto mb-2"></div>
<div className="skeleton h-5 w-48 mx-auto"></div>
</div>
<div className="flex-grow flex flex-col">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 flex-grow">
{/* Skeleton items to match the grid layout */}
<div className="rounded-2xl p-6 shadow-lg border-2 bg-white/90 border-indigo-100">
<div className="flex items-center mb-4">
<div className="skeleton w-12 h-12 rounded-xl mr-4"></div>
<div className="skeleton h-5 w-32"></div>
</div>
<div className="space-y-2">
<div className="flex items-center">
<div className="skeleton h-4 w-16 mr-2"></div>
<div className="skeleton h-4 w-24"></div>
</div>
<div className="pt-2 border-t border-gray-100 mt-2">
<div className="flex items-center mb-1">
<div className="skeleton h-4 w-20 mr-2"></div>
</div>
<div className="ml-6 mt-1 space-y-1">
<div className="flex items-center">
<div className="skeleton h-3 w-12 mr-2"></div>
<div className="skeleton h-3 w-20"></div>
</div>
<div className="flex items-center">
<div className="skeleton h-3 w-12 mr-2"></div>
<div className="skeleton h-3 w-20"></div>
</div>
</div>
</div>
<div className="flex items-center">
<div className="skeleton h-4 w-16 mr-2"></div>
<div className="skeleton h-4 w-32"></div>
</div>
</div>
<div className="mt-4 flex justify-end">
<div className="skeleton w-6 h-6 rounded-full"></div>
</div>
</div>
<div className="rounded-2xl p-6 shadow-lg border-2 bg-white/90 border-indigo-100">
<div className="flex items-center mb-4">
<div className="skeleton w-12 h-12 rounded-xl mr-4"></div>
<div className="skeleton h-5 w-32"></div>
</div>
<div className="space-y-2">
<div className="flex items-center">
<div className="skeleton h-4 w-16 mr-2"></div>
<div className="skeleton h-4 w-24"></div>
</div>
<div className="pt-2 border-t border-gray-100 mt-2">
<div className="flex items-center mb-1">
<div className="skeleton h-4 w-20 mr-2"></div>
</div>
<div className="ml-6 mt-1 space-y-1">
<div className="flex items-center">
<div className="skeleton h-3 w-12 mr-2"></div>
<div className="skeleton h-3 w-20"></div>
</div>
<div className="flex items-center">
<div className="skeleton h-3 w-12 mr-2"></div>
<div className="skeleton h-3 w-20"></div>
</div>
</div>
</div>
<div className="flex items-center">
<div className="skeleton h-4 w-16 mr-2"></div>
<div className="skeleton h-4 w-32"></div>
</div>
</div>
<div className="mt-4 flex justify-end">
<div className="skeleton w-6 h-6 rounded-full"></div>
</div>
</div>
<div className="rounded-2xl p-6 shadow-lg border-2 bg-white/90 border-indigo-100">
<div className="flex items-center mb-4">
<div className="skeleton w-12 h-12 rounded-xl mr-4"></div>
<div className="skeleton h-5 w-32"></div>
</div>
<div className="space-y-2">
<div className="flex items-center">
<div className="skeleton h-4 w-16 mr-2"></div>
<div className="skeleton h-4 w-24"></div>
</div>
<div className="pt-2 border-t border-gray-100 mt-2">
<div className="flex items-center mb-1">
<div className="skeleton h-4 w-20 mr-2"></div>
</div>
<div className="ml-6 mt-1 space-y-1">
<div className="flex items-center">
<div className="skeleton h-3 w-12 mr-2"></div>
<div className="skeleton h-3 w-20"></div>
</div>
<div className="flex items-center">
<div className="skeleton h-3 w-12 mr-2"></div>
<div className="skeleton h-3 w-20"></div>
</div>
</div>
</div>
<div className="flex items-center">
<div className="skeleton h-4 w-16 mr-2"></div>
<div className="skeleton h-4 w-32"></div>
</div>
</div>
<div className="mt-4 flex justify-end">
<div className="skeleton w-6 h-6 rounded-full"></div>
</div>
</div>
</div>
<div className="mt-8 sm:mt-10 flex justify-center">
<div className="skeleton h-14 w-48 rounded-xl"></div>
</div>
</div>
</div>
</div>
</>
)
}
}

View File

@@ -1,3 +1,20 @@
export default function SelectPage() {
return <div></div>;
'use server';
import { Locale } from 'next-intl';
import { checkAccess, checkSelectionOnSelectPage } from '@/app/api/guards';
import SelectPageClient from './SelectPage';
import LocaleSwitcherServer from '@/components/LocaleSwitcherServer';
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 (
<div>
<div className='absolute top-2 right-2'>
<LocaleSwitcherServer locale={locale} pathname="/login" />
</div>
<SelectPageClient />
</div>
);
}

View File

@@ -1,5 +1,38 @@
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);
}
function getLocaleFromPath(path: string) {
const locale = path.split('/')[0];
return locale;
}
export default async function ProtectedLayout({
children,
}: {
children: ReactNode,
}) {
const headersList = await headers();
// const locale = getLocaleFromPath(removeSubStringFromPath(headersList));
const removedLocaleRoute = removeSubStringFromPath(headersList);
// console.log('Removed locale route:', removedLocaleRoute);
return <>{children}</>;
}

View File

@@ -0,0 +1,27 @@
'use server';
import React from 'react';
import { dashboardPages } from '@/pages/office/dashboard/mapper';
import { renderPage } from '@/lib/page';
import { getSelectToken } from '@/fetchers/token/select';
export default async function DashboardPage() {
const pageUrl = "/office/dashboard";
const pageToken = "qY56XMEr08wJkNvOR6EYQZKMVdTQEfHdLXGzzxcKU24E"
const selectToken = await getSelectToken();
try {
const RenderPage = renderPage(selectToken, pageToken, dashboardPages);
if (RenderPage) {
return <>
<div>Dashboard Page</div>
<div className='flex align-center justify-center h-screen w-screen mt-10 text-2xl'>
<RenderPage />
</div>
</>
}
} catch (error) { console.log(error) }
return <>
<div>Dashboard Page</div>
<div>You are not allowed to reach any page under {pageUrl}. Please contact your administrator.</div>
</>;
};

View File

@@ -1,3 +1,3 @@
export default function OfficePage() {
return <div></div>;
return <div>Office Page</div>;
}

View File

@@ -0,0 +1,3 @@
export default function TrialPage() {
return <div>TrialPage</div>;
}

View File

@@ -0,0 +1,23 @@
'use server';
import React from 'react';
import { dashboardPages } from '@/pages/venue/dashboard/mapper';
import { renderPage } from '@/lib/page';
import { getSelectTokenObject } from '@/fetchers/token/select';
export default async function DashboardPage() {
const pageUrl = "/venue/dashboard";
const pageToken = "IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE"
const selectToken = await getSelectTokenObject();
if (selectToken) {
const RenderPage = renderPage(selectToken, pageToken, dashboardPages);
if (RenderPage) {
return <>
<div className='h-screen w-screen text-2xl'><RenderPage /></div>
</>
}
}
return <>
<div>Dashboard Page</div>
<div>You are not allowed to reach any page under {pageUrl}. Please contact your administrator.</div>
</>;
};

View File

@@ -1,3 +1,3 @@
export default function VenuePage() {
return <div></div>;
return <div>Venue Page</div>;
}

View File

@@ -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<typeof selectFormSchema>;
type CreateFormData = z.infer<typeof createFormSchema>;
type SearchFormData = z.infer<typeof searchFormSchema>;
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<Record<string, string>>({});
const [selectFormErrors, setSelectFormErrors] = useState<Record<string, string>>({});
const [searchFormErrors, setSearchFormErrors] = useState<Record<string, string>>({});
const submitCreateForm = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const formDataObj: Record<string, string> = {};
formData.forEach((value, key) => {
formDataObj[key] = value.toString();
});
const result: z.ZodSafeParseResult<CreateFormData> = 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<string, string> = {};
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<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formName = form.getAttribute('name');
console.log('Form name:', formName);
const formData = new FormData(form);
const formDataObj: Record<string, string> = {};
formData.forEach((value, key) => {
formDataObj[key] = value.toString();
});
const result: z.ZodSafeParseResult<SelectFormData> = 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<string, string> = {};
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<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formName = form.getAttribute('name');
console.log('Form name:', formName);
const formData = new FormData(form);
const formDataObj: Record<string, string> = {};
formData.forEach((value, key) => { formDataObj[key] = value.toString() });
const result: z.ZodSafeParseResult<SearchFormData> = 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<string, string> = {};
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 <div className="flex flex-col gap-4">
<div className="flex flex-row gap-4">
<h1 className="text-2xl font-bold">Form Keys</h1>
</div>
<div className="flex justify-center gap-4">
<div className="flex flex-col align-center">
<div className="text-lg my-2">CreateForm: {cacheKeyCreateForm}</div>
<div className="text-lg my-2">SelectForm: {cacheKeySelectForm}</div>
<div className="text-lg my-2">SearchForm: {cacheKeySearchForm}</div>
</div>
</div>
{/* Create a simple Form */}
<div className="flex justify-center gap-4">
<form className="flex flex-col w-1/2 gap-2 p-4 border border-gray-300 rounded" name={cacheKeyCreateForm} onSubmit={submitCreateForm}>
<label className="input w-full my-1">
<p className="text-lg w-24">Name</p>
<input className="w-full h-24" onBlur={handleOnBlurCreateForm} name="inputSelectName" type="text" placeholder="Name Input here" />
<span className="badge badge-neutral badge-xs">Optional</span>
</label>
{createFormErrors.inputSelectName && <p className="text-red-500 text-sm">{createFormErrors.inputSelectName}</p>}
<label className="input w-full my-1 mb-2">
<p className="text-lg w-24">Number</p>
<input className="w-full h-24" onBlur={handleOnBlurCreateForm} name="inputSelectNumber" type="decimal" placeholder="Number Input here" />
<span className="badge badge-neutral badge-xs">Optional</span>
</label>
{createFormErrors.inputSelectNumber && <p className="text-red-500 text-sm">{createFormErrors.inputSelectNumber}</p>}
<button className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded" type="submit">Submit</button>
</form>
{/* Select a simple Form */}
<form className="flex flex-col gap-2 p-4 border border-gray-300 rounded w-1/2" name={cacheKeySelectForm} onSubmit={submitSelectForm}>
<label className="input w-full my-1 mb-2">
<p className="text-lg w-24">Name</p>
<input className="w-full h-24" onBlur={handleOnBlurSelectForm} name="inputSelectName" type="text" placeholder="Name Input here" />
<span className="badge badge-neutral badge-xs">Optional</span>
</label>
{selectFormErrors.inputSelectName && <p className="text-red-500 text-sm">{selectFormErrors.inputSelectName}</p>}
<label className="input w-full my-1 mb-2">
<p className="text-lg w-24">Number</p>
<input className="w-full h-24" onBlur={handleOnBlurSelectForm} name="inputSelectNumber" type="decimal" placeholder="Number Input here" />
<span className="badge badge-neutral badge-xs">Optional</span>
</label>
{selectFormErrors.inputSelectNumber && <p className="text-red-500 text-sm">{selectFormErrors.inputSelectNumber}</p>}
<button className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded" type="submit">Submit</button>
</form>
{/* Search a simple Form */}
<form className="flex flex-col gap-2 p-4 border border-gray-300 rounded w-1/2" name={cacheKeySearchForm} onSubmit={submitSearchForm}>
<label className="input w-full my-1 mb-2">
<p className="text-lg w-24">Name</p>
<input className="w-full h-24" onBlur={handleOnBlurSearchForm} name="inputSearchName" type="text" placeholder="Name Input here" />
<span className="badge badge-neutral badge-xs">Optional</span>
</label>
{searchFormErrors.inputSearchName && <p className="text-red-500 text-sm">{searchFormErrors.inputSearchName}</p>}
<label className="input w-full my-1 mb-2">
<p className="text-lg w-24">Number</p>
<input className="w-full h-24" onBlur={handleOnBlurSearchForm} name="inputSearchNumber" type="decimal" placeholder="Number Input here" />
<span className="badge badge-neutral badge-xs">Optional</span>
</label>
{searchFormErrors.inputSearchNumber && <p className="text-red-500 text-sm">{searchFormErrors.inputSearchNumber}</p>}
<button className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded" type="submit">Submit</button>
</form>
</div>
</div>;
}

View File

@@ -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 (

View File

@@ -1,59 +1,43 @@
import { ReactNode } from 'react';
import { Inter } from 'next/font/google';
import { notFound } from "next/navigation";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { Locale, locales } from "@/i18n/locales";
import { routing } from "@/i18n/routing";
import { NextIntlClientProvider } from 'next-intl';
import '../globals.css';
const inter = Inter({ subsets: ["latin"] });
type Props = {
children: ReactNode;
params: Promise<{ locale: Locale }>;
children: ReactNode;
params: Promise<{ locale: Locale }>;
};
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
return locales.map((locale) => ({ locale }));
}
export async function generateMetadata({ params }: Omit<Props, 'children'>) {
// Properly await params before accessing properties
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'LocaleLayout' });
return {
title: t('title')
};
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'LocaleLayout' });
return { title: t('title') };
}
export default async function LocaleLayout({ children, params }: Props) {
// Properly await params before accessing properties
const { locale } = await params;
// Validate that the incoming locale is valid
if (!locales.includes(locale as Locale)) {
notFound();
}
// Load messages for all child components
const messages = (await import(`@/i18n/${locale}.json`)).default;
// Enable static rendering
// Note: unstable_setRequestLocale is removed as it's causing TypeScript errors
return (
<html lang={locale}>
<body className={inter.className}>
<NextIntlClientProvider
locale={locale}
messages={messages}
timeZone="Europe/Istanbul" // Configure a default timezone for Turkey
>
{children}
</NextIntlClientProvider>
</body>
</html>
);
const { locale } = await params;
if (!locales.includes(locale as Locale)) {
redirect('/' + locales[0]);
}
const messages = (await import(`@/i18n/${locale}.json`)).default;
return (
<>
<html lang={locale}>
<body className={inter.className}>
<NextIntlClientProvider locale={locale} messages={messages} timeZone="Europe/Istanbul">
{children}
</NextIntlClientProvider>
</body>
</html>
</>
);
}

View File

@@ -1,6 +1,8 @@
'use server';
import HomePage from '@/app/home-page';
import { sendChunksToNest } from '@/lib/init-sync';
export default async function Home() {
sendChunksToNest();
return <HomePage />;
}

View File

@@ -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 });
}

View File

@@ -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 });
}

View File

@@ -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" });
}
}

View File

@@ -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 });
console.log("Select response:", response);
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" });
}
}

View File

@@ -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();
}

View File

@@ -0,0 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const { type } = await req.json();
}
export async function GET(req: NextRequest, { params }: { params: Promise<{ type: string }> }) {
const { type } = await params;
}

View File

@@ -0,0 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { deleteKey } from "@/fetchers/redis/redisService";
export async function POST(req: NextRequest) {
const { key } = await req.json();
await deleteKey({ key });
return NextResponse.json({ status: "ok", message: `Deleted "${key}"` });
}

View File

@@ -0,0 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { exists } from "@/fetchers/redis/redisService";
export async function POST(req: NextRequest) {
const { key } = await req.json();
const result = await exists({ key });
return NextResponse.json({ status: "ok", exists: result });
}

View File

@@ -0,0 +1,11 @@
import { NextRequest, NextResponse } from "next/server";
import { getJSON } from "@/fetchers/redis/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 });
}

View File

@@ -0,0 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { updateField } from "@/fetchers/redis/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 });
}
}

View File

@@ -0,0 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { setJSON } from "@/fetchers/redis/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}"` });
}

View File

@@ -0,0 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { updateJSON } from "@/fetchers/redis/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 });
}
}

View File

@@ -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 };

View File

@@ -1,4 +1,10 @@
@import "tailwindcss";
@import "tailwindcss";
@plugin "daisyui";
/* @plugin "daisyui" {
themes: all;
} */
:root {
--background: #ffffff;

View File

@@ -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() {
<main>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
<p>{t('navigation.title')} : {params.locale}</p>
<p>{n('title')} : {params?.locale || 'tr'}</p>
<div className='flex flex-col gap-2'>
<LocaleSwitcherClient />
<button onClick={() => handleNavigation('/about')}>{t('navigation.about')}</button>
<button onClick={() => handleNavigation('/home')}>{t('navigation.home')}</button>
<button onClick={() => handleNavigation('/about')}>{n('about')}</button>
<button onClick={() => handleNavigation('/home')}>{n('home')}</button>
<button onClick={() => handleNavigation('/login')}>{n('login')}</button>
<button onClick={() => handleNavigation('/select')}>{n('select')}</button>
</div>
</main>
);

Some files were not shown because too many files have changed in this diff Show More