Compare commits
27 Commits
ccb5c172ae
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 9dd8740171 | |||
| 4e6774a15b | |||
| e4f6afbc93 | |||
| 61529f7d94 | |||
| 9543d136aa | |||
| 456203f5cf | |||
| 82b1d4825b | |||
| 4ec9031005 | |||
| 7a5521648c | |||
| ca98adc338 | |||
| 405ba2e95d | |||
| 7452e05a92 | |||
| bd12fe02ae | |||
| a00c2942f5 | |||
| 768f0a5daf | |||
| c2fd263f27 | |||
| ac1980566a | |||
| db0ae34948 | |||
| 81184a8acc | |||
| a830cc079d | |||
| a986ddbb95 | |||
| 9232da69d3 | |||
| aa8f0b8f31 | |||
| 1b87dee60d | |||
| b54bbe2db2 | |||
| 924b538559 | |||
| 0ce522d04a |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -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
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
}
|
||||
12
README.md
12
README.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
1478
ServicesApi/package-lock.json
generated
1478
ServicesApi/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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() { }
|
||||
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
54
ServicesApi/src/accounts/superusers/superusers.service.ts
Normal file
54
ServicesApi/src/accounts/superusers/superusers.service.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsObject, IsOptional, IsString, IsBoolean } from 'class-validator';
|
||||
import { IsOptional, IsString, IsBoolean } from 'class-validator';
|
||||
|
||||
export class userLoginValidator {
|
||||
@IsString()
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -29,5 +29,4 @@ export class VerifyOtpService {
|
||||
// // controller veya resolver içinden
|
||||
// const { secret, otpauthUrl } = authService.generate2FASecret('mehmet');
|
||||
// const qrCodeImage = await authService.generateQRCode(otpauthUrl);
|
||||
|
||||
// // qrCodeImage → frontend’e gönder, <img src="data:image/png;base64,..."> diye gösterilebilir
|
||||
|
||||
@@ -2,5 +2,5 @@ import { IsString } from 'class-validator';
|
||||
|
||||
export class userSelectValidator {
|
||||
@IsString()
|
||||
selected_uu_id: string;
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
@@ -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') }
|
||||
}
|
||||
}
|
||||
|
||||
9
ServicesApi/src/database/mongo/mongo.module.ts
Normal file
9
ServicesApi/src/database/mongo/mongo.module.ts
Normal 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 {}
|
||||
11
ServicesApi/src/database/mongo/mongo.provider.ts
Normal file
11
ServicesApi/src/database/mongo/mongo.provider.ts
Normal 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');
|
||||
},
|
||||
};
|
||||
18
ServicesApi/src/database/mongo/mongo.service.spec.ts
Normal file
18
ServicesApi/src/database/mongo/mongo.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
294
ServicesApi/src/database/mongo/mongo.service.ts
Normal file
294
ServicesApi/src/database/mongo/mongo.service.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
2
ServicesApi/src/database/redis/redis.constants.ts
Normal file
2
ServicesApi/src/database/redis/redis.constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const REDIS_CLIENT = 'REDIS_CLIENT';
|
||||
export const REDIS_OPTIONS = 'REDIS_OPTIONS';
|
||||
23
ServicesApi/src/database/redis/redis.interfaces.ts
Normal file
23
ServicesApi/src/database/redis/redis.interfaces.ts
Normal 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[];
|
||||
}
|
||||
91
ServicesApi/src/database/redis/redis.module.ts
Normal file
91
ServicesApi/src/database/redis/redis.module.ts
Normal 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() }
|
||||
}
|
||||
}
|
||||
30
ServicesApi/src/database/redis/redis.service.spec.ts
Normal file
30
ServicesApi/src/database/redis/redis.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
47
ServicesApi/src/navigator/dtoValidator.ts
Normal file
47
ServicesApi/src/navigator/dtoValidator.ts
Normal 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;
|
||||
}
|
||||
41
ServicesApi/src/navigator/events/dtoValidator.ts
Normal file
41
ServicesApi/src/navigator/events/dtoValidator.ts
Normal 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;
|
||||
}
|
||||
18
ServicesApi/src/navigator/events/events.service.spec.ts
Normal file
18
ServicesApi/src/navigator/events/events.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
214
ServicesApi/src/navigator/events/events.service.ts
Normal file
214
ServicesApi/src/navigator/events/events.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
37
ServicesApi/src/navigator/events/validation-example.ts
Normal file
37
ServicesApi/src/navigator/events/validation-example.ts
Normal 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));
|
||||
48
ServicesApi/src/navigator/menus/main.ts
Normal file
48
ServicesApi/src/navigator/menus/main.ts
Normal 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))
|
||||
};
|
||||
|
||||
18
ServicesApi/src/navigator/menus/menu.service.spec.ts
Normal file
18
ServicesApi/src/navigator/menus/menu.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
56
ServicesApi/src/navigator/menus/menu.service.ts
Normal file
56
ServicesApi/src/navigator/menus/menu.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
301
ServicesApi/src/navigator/menus/menuItems/employee.ts
Normal file
301
ServicesApi/src/navigator/menus/menuItems/employee.ts
Normal 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: [],
|
||||
}
|
||||
],
|
||||
}
|
||||
];
|
||||
13
ServicesApi/src/navigator/menus/menuItems/occupant.ts
Normal file
13
ServicesApi/src/navigator/menus/menuItems/occupant.ts
Normal 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: [],
|
||||
},
|
||||
]
|
||||
13
ServicesApi/src/navigator/menus/test.ts
Normal file
13
ServicesApi/src/navigator/menus/test.ts
Normal 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));
|
||||
18
ServicesApi/src/navigator/navigator.controller.spec.ts
Normal file
18
ServicesApi/src/navigator/navigator.controller.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
73
ServicesApi/src/navigator/navigator.controller.ts
Normal file
73
ServicesApi/src/navigator/navigator.controller.ts
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
18
ServicesApi/src/navigator/navigator.module.ts
Normal file
18
ServicesApi/src/navigator/navigator.module.ts
Normal 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() { }
|
||||
}
|
||||
18
ServicesApi/src/navigator/pages/pages.service.spec.ts
Normal file
18
ServicesApi/src/navigator/pages/pages.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
172
ServicesApi/src/navigator/pages/pages.service.ts
Normal file
172
ServicesApi/src/navigator/pages/pages.service.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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({
|
||||
|
||||
63
ServicesApi/src/utils/navigator/navigator.ts
Normal file
63
ServicesApi/src/utils/navigator/navigator.ts
Normal 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.`) }
|
||||
}
|
||||
}
|
||||
67
ServicesApi/src/utils/navigator/urlHandler.ts
Normal file
67
ServicesApi/src/utils/navigator/urlHandler.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
6
ServicesApi/src/utils/store/mongoHandler.ts
Normal file
6
ServicesApi/src/utils/store/mongoHandler.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class MongoHandler {
|
||||
constructor() { }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
18
ServicesApi/src/utils/types/menus.ts
Normal file
18
ServicesApi/src/utils/types/menus.ts
Normal 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;
|
||||
}
|
||||
16
ServicesApi/src/utils/types/url.ts
Normal file
16
ServicesApi/src/utils/types/url.ts
Normal 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;
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -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": {
|
||||
"@/*": ["*"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
594
ServicesFrontEnd/frontend/package-lock.json
generated
594
ServicesFrontEnd/frontend/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}</>;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>;
|
||||
};
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function OfficePage() {
|
||||
return <div></div>;
|
||||
return <div>Office Page</div>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function TrialPage() {
|
||||
return <div>TrialPage</div>;
|
||||
}
|
||||
@@ -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>
|
||||
</>;
|
||||
};
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function VenuePage() {
|
||||
return <div></div>;
|
||||
return <div>Venue Page</div>;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
0
ServicesFrontEnd/frontend/src/app/api/a.txt
Normal file
0
ServicesFrontEnd/frontend/src/app/api/a.txt
Normal 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 });
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
28
ServicesFrontEnd/frontend/src/app/api/auth/login/route.ts
Normal file
28
ServicesFrontEnd/frontend/src/app/api/auth/login/route.ts
Normal 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" });
|
||||
}
|
||||
}
|
||||
36
ServicesFrontEnd/frontend/src/app/api/auth/select/route.ts
Normal file
36
ServicesFrontEnd/frontend/src/app/api/auth/select/route.ts
Normal 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" });
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
8
ServicesFrontEnd/frontend/src/app/api/cache/delete/route.ts
vendored
Normal file
8
ServicesFrontEnd/frontend/src/app/api/cache/delete/route.ts
vendored
Normal 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}"` });
|
||||
}
|
||||
8
ServicesFrontEnd/frontend/src/app/api/cache/exists/route.ts
vendored
Normal file
8
ServicesFrontEnd/frontend/src/app/api/cache/exists/route.ts
vendored
Normal 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 });
|
||||
}
|
||||
11
ServicesFrontEnd/frontend/src/app/api/cache/get/route.ts
vendored
Normal file
11
ServicesFrontEnd/frontend/src/app/api/cache/get/route.ts
vendored
Normal 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 });
|
||||
}
|
||||
12
ServicesFrontEnd/frontend/src/app/api/cache/renew/route.ts
vendored
Normal file
12
ServicesFrontEnd/frontend/src/app/api/cache/renew/route.ts
vendored
Normal 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 });
|
||||
}
|
||||
}
|
||||
8
ServicesFrontEnd/frontend/src/app/api/cache/set/route.ts
vendored
Normal file
8
ServicesFrontEnd/frontend/src/app/api/cache/set/route.ts
vendored
Normal 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}"` });
|
||||
}
|
||||
12
ServicesFrontEnd/frontend/src/app/api/cache/update/route.ts
vendored
Normal file
12
ServicesFrontEnd/frontend/src/app/api/cache/update/route.ts
vendored
Normal 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 });
|
||||
}
|
||||
}
|
||||
35
ServicesFrontEnd/frontend/src/app/api/guards.tsx
Normal file
35
ServicesFrontEnd/frontend/src/app/api/guards.tsx
Normal 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 };
|
||||
@@ -1,4 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
|
||||
/* @plugin "daisyui" {
|
||||
themes: all;
|
||||
} */
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user