Compare commits
No commits in common. "924b53855939fbf245a728022e5fd19dbd2b1968" and "ccb5c172ae10e7e01338d0edb416f8c440c03af1" have entirely different histories.
924b538559
...
ccb5c172ae
12
README.md
12
README.md
|
|
@ -48,15 +48,3 @@ npx prisma db push # update remote schema # not good for production creates no s
|
||||||
|
|
||||||
npx prisma validate
|
npx prisma validate
|
||||||
npx prisma format
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -3508,7 +3508,6 @@ 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
|
/// 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 {
|
model users {
|
||||||
user_tag String @default("") @db.VarChar(64)
|
user_tag String @default("") @db.VarChar(64)
|
||||||
user_type String @default("employee") @db.VarChar(32)
|
|
||||||
email String @default("") @db.VarChar(128)
|
email String @default("") @db.VarChar(128)
|
||||||
phone_number String @default("") @db.VarChar
|
phone_number String @default("") @db.VarChar
|
||||||
via String @default("111") @db.VarChar
|
via String @default("111") @db.VarChar
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ export class AuthController {
|
||||||
@Post('select')
|
@Post('select')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@UseGuards(AuthControlGuard)
|
@UseGuards(AuthControlGuard)
|
||||||
async select(@Body() query: userSelectValidator, @Req() req: Request) {
|
async select(@Body() query: userSelectValidator) {
|
||||||
return await this.authService.select(query, req);
|
return { message: 'Logout successful' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/password/create')
|
@Post('/password/create')
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ export class AuthService {
|
||||||
return await this.logoutService.run(dto);
|
return await this.logoutService.run(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async select(dto: userSelectValidator, req: Request) {
|
async select(dto: userSelectValidator) {
|
||||||
return await this.selectService.run(dto, req);
|
return await this.selectService.run(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPassword(dto: any) {
|
async createPassword(dto: any) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { IsOptional, IsString, IsBoolean } from 'class-validator';
|
import { IsObject, IsOptional, IsString, IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
export class userLoginValidator {
|
export class userLoginValidator {
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|
|
||||||
|
|
@ -17,138 +17,31 @@ export class LoginService {
|
||||||
const foundUser = await this.prisma.users.findFirstOrThrow({
|
const foundUser = await this.prisma.users.findFirstOrThrow({
|
||||||
where: { email: dto.accessKey },
|
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(
|
const isPasswordValid = this.passHandlers.check_password(
|
||||||
foundUser.uu_id,
|
foundUser.uu_id,
|
||||||
dto.password,
|
dto.password,
|
||||||
foundUser.hash_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({
|
const foundPerson = await this.prisma.people.findFirstOrThrow({
|
||||||
where: { id: foundUser.id },
|
where: { id: foundUser.id },
|
||||||
});
|
});
|
||||||
const alreadyExists = await this.redis.callExistingLoginToken(
|
|
||||||
foundUser.uu_id,
|
|
||||||
);
|
|
||||||
if (alreadyExists) {
|
|
||||||
return {
|
|
||||||
token: alreadyExists,
|
|
||||||
message: 'User already logged in',
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
let selectList: any[] = [];
|
|
||||||
if (foundUser.user_type === 'occupant') {
|
|
||||||
const livingSpaces = await this.prisma.build_living_space.findMany({
|
|
||||||
where: { people: { id: foundPerson.id } },
|
|
||||||
orderBy: {
|
|
||||||
build_parts: {
|
|
||||||
build: {
|
|
||||||
id: 'asc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
occupant_types: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
occupant_code: true,
|
|
||||||
occupant_type: true,
|
|
||||||
function_retriever: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
build_parts: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
part_code: true,
|
|
||||||
part_no: true,
|
|
||||||
part_level: true,
|
|
||||||
human_livable: true,
|
|
||||||
api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
enum_class: true,
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
build_name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
selectList = livingSpaces;
|
|
||||||
} else if (foundUser.user_type === 'employee') {
|
|
||||||
const employees = await this.prisma.employees.findMany({
|
|
||||||
where: { people: { id: foundPerson.id } },
|
|
||||||
orderBy: {
|
|
||||||
staff: {
|
|
||||||
duties: {
|
|
||||||
departments: {
|
|
||||||
companies: {
|
|
||||||
formal_name: 'asc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
staff: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
staff_code: true,
|
|
||||||
function_retriever: true,
|
|
||||||
duties: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
departments: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
department_code: true,
|
|
||||||
department_name: true,
|
|
||||||
companies: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
formal_name: true,
|
|
||||||
public_name: true,
|
|
||||||
addresses: {
|
|
||||||
select: {
|
|
||||||
comment_address: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
selectList = employees;
|
|
||||||
}
|
|
||||||
|
|
||||||
const redisData = AuthTokenSchema.parse({
|
const redisData = AuthTokenSchema.parse({
|
||||||
people: foundPerson,
|
people: foundPerson,
|
||||||
users: foundUser,
|
users: foundUser,
|
||||||
credentials: {
|
credentials: {
|
||||||
person_uu_id: foundPerson.uu_id,
|
person_id: foundPerson.id,
|
||||||
person_name: foundPerson.firstname,
|
person_name: foundPerson.firstname,
|
||||||
person_full_name: `${foundPerson.firstname} ${foundPerson.middle_name || ''} | ${foundPerson.birthname || ''} | ${foundPerson.surname}`,
|
|
||||||
},
|
|
||||||
selectionList: {
|
|
||||||
type: foundUser.user_type,
|
|
||||||
list: selectList,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -157,9 +50,8 @@ export class LoginService {
|
||||||
foundUser.uu_id,
|
foundUser.uu_id,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
token: accessToken,
|
accessToken,
|
||||||
message: 'Login successful',
|
message: 'Login successful',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,5 @@ export class VerifyOtpService {
|
||||||
// // controller veya resolver içinden
|
// // controller veya resolver içinden
|
||||||
// const { secret, otpauthUrl } = authService.generate2FASecret('mehmet');
|
// const { secret, otpauthUrl } = authService.generate2FASecret('mehmet');
|
||||||
// const qrCodeImage = await authService.generateQRCode(otpauthUrl);
|
// const qrCodeImage = await authService.generateQRCode(otpauthUrl);
|
||||||
|
|
||||||
// // qrCodeImage → frontend’e gönder, <img src="data:image/png;base64,..."> diye gösterilebilir
|
// // 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 {
|
export class userSelectValidator {
|
||||||
@IsString()
|
@IsString()
|
||||||
uuid: string;
|
selected_uu_id: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,254 +1,9 @@
|
||||||
import {
|
import { Injectable } from '@nestjs/common';
|
||||||
Injectable,
|
|
||||||
BadRequestException,
|
|
||||||
UnauthorizedException,
|
|
||||||
NotAcceptableException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { userSelectValidator } from '@/src/auth/select/dtoValidator';
|
import { userSelectValidator } from '@/src/auth/select/dtoValidator';
|
||||||
import { RedisHandlers } from '@/src/utils/auth/redis_handlers';
|
|
||||||
import {
|
|
||||||
EmployeeTokenSchema,
|
|
||||||
OccupantTokenSchema,
|
|
||||||
TokenDictInterface,
|
|
||||||
UserType,
|
|
||||||
} from '@/src/types/auth/token';
|
|
||||||
import { PrismaService } from '@/src/prisma.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SelectService {
|
export class SelectService {
|
||||||
constructor(
|
async run(dto: userSelectValidator) {
|
||||||
private readonly redis: RedisHandlers,
|
return dto;
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
) {}
|
|
||||||
async run(dto: userSelectValidator, req: Request) {
|
|
||||||
const accessObject = await this.redis.getLoginFromRedis(req);
|
|
||||||
if (!accessObject) {
|
|
||||||
throw new UnauthorizedException(
|
|
||||||
'Authorization failed. Please login to continue',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const accessToken = accessObject.key.split(':')[1];
|
|
||||||
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: { id: employee.staff_id },
|
|
||||||
omit: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const duties = await this.prisma.duties.findFirstOrThrow({
|
|
||||||
where: { id: staff.duties_id },
|
|
||||||
omit: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const department = await this.prisma.departments.findFirstOrThrow({
|
|
||||||
where: { id: duties.department_id },
|
|
||||||
omit: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const duty = await this.prisma.duty.findFirstOrThrow({
|
|
||||||
where: { id: duties.duties_id },
|
|
||||||
omit: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const company = await this.prisma.companies.findFirstOrThrow({
|
|
||||||
where: { id: duties.company_id },
|
|
||||||
omit: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const employeeToken = EmployeeTokenSchema.parse({
|
|
||||||
company: company,
|
|
||||||
department: department,
|
|
||||||
duty: duty,
|
|
||||||
employee: employee,
|
|
||||||
staff: staff,
|
|
||||||
menu: null,
|
|
||||||
pages: null,
|
|
||||||
selection: await this.prisma.employees.findFirstOrThrow({
|
|
||||||
where: { uu_id: dto.uuid },
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
staff: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
staff_code: true,
|
|
||||||
function_retriever: true,
|
|
||||||
duties: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
departments: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
department_code: true,
|
|
||||||
department_name: true,
|
|
||||||
companies: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
formal_name: true,
|
|
||||||
public_name: true,
|
|
||||||
addresses: {
|
|
||||||
select: {
|
|
||||||
comment_address: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
functionsRetriever: staff.function_retriever,
|
|
||||||
kind: UserType.employee,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 },
|
|
||||||
omit: {
|
|
||||||
id: true,
|
|
||||||
person_id: true,
|
|
||||||
build_parts_id: true,
|
|
||||||
occupant_type_id: true,
|
|
||||||
ref_id: true,
|
|
||||||
replication_id: true,
|
|
||||||
cryp_uu_id: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const occupantType = await this.prisma.occupant_types.findFirstOrThrow({
|
|
||||||
where: { uu_id: livingSpace.occupant_type_uu_id },
|
|
||||||
omit: {
|
|
||||||
id: true,
|
|
||||||
cryp_uu_id: true,
|
|
||||||
ref_id: true,
|
|
||||||
replication_id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const part = await this.prisma.build_parts.findFirstOrThrow({
|
|
||||||
where: { uu_id: livingSpace.build_parts_uu_id },
|
|
||||||
omit: {
|
|
||||||
id: true,
|
|
||||||
cryp_uu_id: true,
|
|
||||||
ref_id: true,
|
|
||||||
replication_id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const build = await this.prisma.build.findFirstOrThrow({
|
|
||||||
where: { uu_id: part.build_uu_id },
|
|
||||||
omit: {
|
|
||||||
id: true,
|
|
||||||
cryp_uu_id: true,
|
|
||||||
ref_id: true,
|
|
||||||
replication_id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const company = await this.prisma.companies.findFirstOrThrow({
|
|
||||||
where: { uu_id: accessObject.value.users.related_company },
|
|
||||||
omit: {
|
|
||||||
id: true,
|
|
||||||
cryp_uu_id: true,
|
|
||||||
ref_id: true,
|
|
||||||
replication_id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const occupantToken = OccupantTokenSchema.parse({
|
|
||||||
livingSpace: livingSpace,
|
|
||||||
occupant: occupantType,
|
|
||||||
build: build,
|
|
||||||
part: part,
|
|
||||||
company: company,
|
|
||||||
menu: null,
|
|
||||||
pages: null,
|
|
||||||
config: null,
|
|
||||||
caches: null,
|
|
||||||
selection: await this.prisma.build_living_space.findFirstOrThrow({
|
|
||||||
where: { uu_id: dto.uuid },
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
occupant_types: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
occupant_code: true,
|
|
||||||
occupant_type: true,
|
|
||||||
function_retriever: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
build_parts: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
part_code: true,
|
|
||||||
part_no: true,
|
|
||||||
part_level: true,
|
|
||||||
human_livable: true,
|
|
||||||
api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
enum_class: true,
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
select: {
|
|
||||||
uu_id: true,
|
|
||||||
build_name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
functionsRetriever: occupantType.function_retriever,
|
|
||||||
kind: UserType.occupant,
|
|
||||||
});
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,16 @@ export class CacheService {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { key, value: JSON.parse(value) };
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
async set_with_ttl(key: string, value: any, ttl: number) {
|
async set_with_ttl(key: string, value: any, ttl: number) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
await app.listen(process.env.PORT ?? 8001);
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
|
|
||||||
console.log(`🚀 Uygulama çalışıyor: ${await app.getUrl()}`);
|
console.log(`🚀 Uygulama çalışıyor: ${await app.getUrl()}`);
|
||||||
extractAndPersistRoutes(app, app.get(PrismaService));
|
extractAndPersistRoutes(app, app.get(PrismaService));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,14 @@ export class AuthControlGuard implements CanActivate {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
const accessToken = this.cacheService.mergeLoginKey(req);
|
const accessToken = this.cacheService.mergeLoginKey(req);
|
||||||
console.log('AuthControlGuard', accessToken);
|
console.log('AuthControlGuard', accessToken);
|
||||||
|
// const hasAccess = accessObject.permissions?.some(
|
||||||
|
// (p: any) => p.method === method && p.url === path,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (!hasAccess) {
|
||||||
|
// throw new ForbiddenException('Access denied to this route');
|
||||||
|
// }
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -24,11 +32,18 @@ export class EndpointControlGuard implements CanActivate {
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
// const selectToken = this.cacheService.mergeSelectKey(req);
|
const selectToken = this.cacheService.mergeSelectKey(req);
|
||||||
// const method = req.method;
|
const method = req.method;
|
||||||
// const path = req.route?.path;
|
const path = req.route?.path;
|
||||||
const accessObject = await this.cacheService.getSelectFromRedis(req);
|
console.log('EndpointControlGuard', selectToken, method, path);
|
||||||
console.log('EndpointControlGuard', accessObject);
|
// const hasAccess = accessObject.permissions?.some(
|
||||||
|
// (p: any) => p.method === method && p.url === path,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (!hasAccess) {
|
||||||
|
// throw new ForbiddenException('Access denied to this route');
|
||||||
|
// }
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,8 @@ export type UserType = (typeof UserType)[keyof typeof UserType];
|
||||||
|
|
||||||
// Credentials
|
// Credentials
|
||||||
export const CredentialsSchema = z.object({
|
export const CredentialsSchema = z.object({
|
||||||
person_uu_id: z.string(),
|
person_id: z.number(),
|
||||||
person_name: z.string(),
|
person_name: z.string(),
|
||||||
full_name: z.string(),
|
|
||||||
});
|
});
|
||||||
export type Credentials = z.infer<typeof CredentialsSchema>;
|
export type Credentials = z.infer<typeof CredentialsSchema>;
|
||||||
|
|
||||||
|
|
@ -27,7 +26,7 @@ export const AuthTokenSchema = z.object({
|
||||||
father_name: z.string(),
|
father_name: z.string(),
|
||||||
mother_name: z.string(),
|
mother_name: z.string(),
|
||||||
country_code: z.string(),
|
country_code: z.string(),
|
||||||
// national_identity_id: z.string(),
|
national_identity_id: z.string(),
|
||||||
birth_place: z.string(),
|
birth_place: z.string(),
|
||||||
birth_date: z.date(),
|
birth_date: z.date(),
|
||||||
tax_no: z.string(),
|
tax_no: z.string(),
|
||||||
|
|
@ -43,7 +42,7 @@ export const AuthTokenSchema = z.object({
|
||||||
active: z.boolean(),
|
active: z.boolean(),
|
||||||
is_notification_send: z.boolean(),
|
is_notification_send: z.boolean(),
|
||||||
is_email_send: z.boolean(),
|
is_email_send: z.boolean(),
|
||||||
// id: z.number(),
|
id: z.number(),
|
||||||
uu_id: z.string(),
|
uu_id: z.string(),
|
||||||
expiry_starts: z.date(),
|
expiry_starts: z.date(),
|
||||||
expiry_ends: z.date(),
|
expiry_ends: z.date(),
|
||||||
|
|
@ -53,11 +52,10 @@ export const AuthTokenSchema = z.object({
|
||||||
users: z.object({
|
users: z.object({
|
||||||
user_tag: z.string(),
|
user_tag: z.string(),
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
user_type: z.string(),
|
|
||||||
phone_number: z.string(),
|
phone_number: z.string(),
|
||||||
via: z.string(),
|
via: z.string(),
|
||||||
avatar: z.string(),
|
avatar: z.string(),
|
||||||
// hash_password: z.string(),
|
hash_password: z.string(),
|
||||||
password_token: z.string(),
|
password_token: z.string(),
|
||||||
remember_me: z.boolean(),
|
remember_me: z.boolean(),
|
||||||
password_expires_day: z.number(),
|
password_expires_day: z.number(),
|
||||||
|
|
@ -78,7 +76,7 @@ export const AuthTokenSchema = z.object({
|
||||||
active: z.boolean(),
|
active: z.boolean(),
|
||||||
is_notification_send: z.boolean(),
|
is_notification_send: z.boolean(),
|
||||||
is_email_send: z.boolean(),
|
is_email_send: z.boolean(),
|
||||||
// id: z.number(),
|
id: z.number(),
|
||||||
uu_id: z.string(),
|
uu_id: z.string(),
|
||||||
expiry_starts: z.date(),
|
expiry_starts: z.date(),
|
||||||
expiry_ends: z.date(),
|
expiry_ends: z.date(),
|
||||||
|
|
@ -87,178 +85,33 @@ export const AuthTokenSchema = z.object({
|
||||||
default_language: z.string(),
|
default_language: z.string(),
|
||||||
}),
|
}),
|
||||||
credentials: CredentialsSchema,
|
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 type AuthToken = z.infer<typeof AuthTokenSchema>;
|
||||||
|
|
||||||
export const EmployeeTokenSchema = z.object({
|
export const EmployeeTokenSchema = z.object({
|
||||||
company: z.object({
|
|
||||||
// id: z.number(),
|
|
||||||
uu_id: z.string(),
|
|
||||||
formal_name: z.string(),
|
|
||||||
company_type: z.string(),
|
|
||||||
commercial_type: z.string(),
|
|
||||||
tax_no: z.string(),
|
|
||||||
public_name: z.string(),
|
|
||||||
company_tag: z.string(),
|
|
||||||
default_lang_type: z.string(),
|
|
||||||
default_money_type: z.string(),
|
|
||||||
is_commercial: z.boolean(),
|
|
||||||
is_blacklist: z.boolean(),
|
|
||||||
parent_id: z.number().nullable(),
|
|
||||||
workplace_no: z.string().nullable(),
|
|
||||||
// official_address_id: z.number().nullable(),
|
|
||||||
official_address_uu_id: z.string().nullable(),
|
|
||||||
top_responsible_company_id: z.number().nullable(),
|
|
||||||
top_responsible_company_uu_id: z.string().nullable(),
|
|
||||||
ref_id: z.string().nullable(),
|
|
||||||
// replication_id: z.number(),
|
|
||||||
// cryp_uu_id: z.string().nullable(),
|
|
||||||
created_credentials_token: z.string().nullable(),
|
|
||||||
updated_credentials_token: z.string().nullable(),
|
|
||||||
confirmed_credentials_token: z.string().nullable(),
|
|
||||||
is_confirmed: z.boolean(),
|
|
||||||
deleted: z.boolean(),
|
|
||||||
active: z.boolean(),
|
|
||||||
is_notification_send: z.boolean(),
|
|
||||||
is_email_send: z.boolean(),
|
|
||||||
expiry_starts: z.date(),
|
|
||||||
expiry_ends: z.date(),
|
|
||||||
created_at: z.date(),
|
|
||||||
updated_at: z.date(),
|
|
||||||
ref_int: z.number().nullable(),
|
|
||||||
}),
|
|
||||||
department: z.object({
|
|
||||||
// id: z.number(),
|
|
||||||
uu_id: z.string(),
|
|
||||||
parent_department_id: z.number().nullable(),
|
|
||||||
department_code: z.string(),
|
|
||||||
department_name: z.string(),
|
|
||||||
department_description: z.string(),
|
|
||||||
// company_id: z.number(),
|
|
||||||
company_uu_id: z.string(),
|
|
||||||
// ref_id: z.string().nullable(),
|
|
||||||
// replication_id: z.number(),
|
|
||||||
// cryp_uu_id: z.string().nullable(),
|
|
||||||
created_credentials_token: z.string().nullable(),
|
|
||||||
updated_credentials_token: z.string().nullable(),
|
|
||||||
confirmed_credentials_token: z.string().nullable(),
|
|
||||||
is_confirmed: z.boolean(),
|
|
||||||
deleted: z.boolean(),
|
|
||||||
active: z.boolean(),
|
|
||||||
is_notification_send: z.boolean(),
|
|
||||||
is_email_send: z.boolean(),
|
|
||||||
expiry_starts: z.date(),
|
|
||||||
expiry_ends: z.date(),
|
|
||||||
created_at: z.date(),
|
|
||||||
updated_at: z.date(),
|
|
||||||
ref_int: z.number().nullable(),
|
|
||||||
}),
|
|
||||||
duty: z.object({
|
|
||||||
// id: z.number(),
|
|
||||||
uu_id: z.string(),
|
|
||||||
duty_name: z.string(),
|
|
||||||
duty_code: z.string(),
|
|
||||||
duty_description: z.string(),
|
|
||||||
// ref_id: z.string().nullable(),
|
|
||||||
// replication_id: z.number(),
|
|
||||||
// cryp_uu_id: z.string().nullable(),
|
|
||||||
created_credentials_token: z.string().nullable(),
|
|
||||||
updated_credentials_token: z.string().nullable(),
|
|
||||||
confirmed_credentials_token: z.string().nullable(),
|
|
||||||
is_confirmed: z.boolean(),
|
|
||||||
deleted: z.boolean(),
|
|
||||||
active: z.boolean(),
|
|
||||||
is_notification_send: z.boolean(),
|
|
||||||
is_email_send: z.boolean(),
|
|
||||||
expiry_starts: z.date(),
|
|
||||||
expiry_ends: z.date(),
|
|
||||||
created_at: z.date(),
|
|
||||||
updated_at: z.date(),
|
|
||||||
ref_int: z.number().nullable(),
|
|
||||||
}),
|
|
||||||
employee: z.object({
|
|
||||||
// id: z.number(),
|
|
||||||
uu_id: z.string(),
|
|
||||||
staff_id: z.number(),
|
|
||||||
staff_uu_id: z.string(),
|
|
||||||
people_id: z.number(),
|
|
||||||
people_uu_id: z.string(),
|
|
||||||
// ref_id: z.string().nullable(),
|
|
||||||
// replication_id: z.number(),
|
|
||||||
// cryp_uu_id: z.string().nullable(),
|
|
||||||
created_credentials_token: z.string().nullable(),
|
|
||||||
updated_credentials_token: z.string().nullable(),
|
|
||||||
confirmed_credentials_token: z.string().nullable(),
|
|
||||||
is_confirmed: z.boolean(),
|
|
||||||
deleted: z.boolean(),
|
|
||||||
active: z.boolean(),
|
|
||||||
is_notification_send: z.boolean(),
|
|
||||||
is_email_send: z.boolean(),
|
|
||||||
expiry_starts: z.date(),
|
|
||||||
expiry_ends: z.date(),
|
|
||||||
created_at: z.date(),
|
|
||||||
updated_at: z.date(),
|
|
||||||
ref_int: z.number().nullable(),
|
|
||||||
}),
|
|
||||||
staff: z.object({
|
|
||||||
// id: z.number(),
|
|
||||||
uu_id: z.string(),
|
|
||||||
staff_description: z.string(),
|
|
||||||
staff_name: z.string(),
|
|
||||||
staff_code: z.string(),
|
|
||||||
// duties_id: z.number(),
|
|
||||||
duties_uu_id: z.string(),
|
|
||||||
function_retriever: z.string().nullable(),
|
|
||||||
// ref_id: z.string().nullable(),
|
|
||||||
// replication_id: z.number(),
|
|
||||||
// cryp_uu_id: z.string().nullable(),
|
|
||||||
created_credentials_token: z.string().nullable(),
|
|
||||||
updated_credentials_token: z.string().nullable(),
|
|
||||||
confirmed_credentials_token: z.string().nullable(),
|
|
||||||
is_confirmed: z.boolean(),
|
|
||||||
deleted: z.boolean(),
|
|
||||||
active: z.boolean(),
|
|
||||||
is_notification_send: z.boolean(),
|
|
||||||
is_email_send: z.boolean(),
|
|
||||||
expiry_starts: z.date(),
|
|
||||||
expiry_ends: z.date(),
|
|
||||||
created_at: z.date(),
|
|
||||||
updated_at: z.date(),
|
|
||||||
ref_int: z.number().nullable(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
menu: z.array(z.object({})).nullable(),
|
|
||||||
pages: z.array(z.string()).nullable(),
|
|
||||||
|
|
||||||
selection: z.record(z.string(), z.unknown()).nullable(),
|
|
||||||
functionsRetriever: 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),
|
kind: z.literal(UserType.employee),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const OccupantTokenSchema = z.object({
|
export const OccupantTokenSchema = z.object({
|
||||||
livingSpace: z.object({}),
|
|
||||||
occupant: z.object({}),
|
|
||||||
build: z.object({}),
|
|
||||||
part: z.object({}),
|
|
||||||
company: z.object({}).optional(),
|
|
||||||
|
|
||||||
menu: z.array(z.object({})).nullable(),
|
|
||||||
pages: z.array(z.string()).nullable(),
|
|
||||||
|
|
||||||
selection: z.record(z.string(), z.unknown()).nullable(),
|
|
||||||
functionsRetriever: z.string(),
|
functionsRetriever: z.string(),
|
||||||
|
livingSpace: z.object({}),
|
||||||
|
occupantType: z.object({}),
|
||||||
|
build: z.object({}),
|
||||||
|
buildPart: z.object({}),
|
||||||
|
responsibleCompany: z.object({}).optional(),
|
||||||
|
responsibleEmployee: z.object({}).optional(),
|
||||||
kind: z.literal(UserType.occupant),
|
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', [
|
export const TokenDictTypes = z.discriminatedUnion('kind', [
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import {
|
||||||
TokenDictTypes,
|
TokenDictTypes,
|
||||||
TokenDictInterface,
|
TokenDictInterface,
|
||||||
AuthToken,
|
AuthToken,
|
||||||
AuthTokenSchema,
|
UserType,
|
||||||
} from '@/src/types/auth/token';
|
} from '@/src/types/auth/token';
|
||||||
import { CacheService } from '@/src/cache.service';
|
import { CacheService } from '@/src/cache.service';
|
||||||
|
import { users } from '@prisma/client';
|
||||||
import { PasswordHandlers } from './login_handler';
|
import { PasswordHandlers } from './login_handler';
|
||||||
import { Injectable, ForbiddenException } from '@nestjs/common';
|
import { Injectable, ForbiddenException } from '@nestjs/common';
|
||||||
|
|
||||||
|
|
@ -21,7 +22,6 @@ interface SelectFromRedis {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RedisHandlers {
|
export class RedisHandlers {
|
||||||
AUTH_TOKEN = 'AUTH_TOKEN';
|
AUTH_TOKEN = 'AUTH_TOKEN';
|
||||||
SELECT_TOKEN = 'SELECT_TOKEN';
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly cacheService: CacheService,
|
private readonly cacheService: CacheService,
|
||||||
private readonly passwordService: PasswordHandlers,
|
private readonly passwordService: PasswordHandlers,
|
||||||
|
|
@ -32,10 +32,10 @@ export class RedisHandlers {
|
||||||
* Format: AUTH_TOKEN:token:token:UUID or AUTH_TOKEN:token:token:*:*
|
* Format: AUTH_TOKEN:token:token:UUID or AUTH_TOKEN:token:token:*:*
|
||||||
this.AUTH_TOKEN:token:token:UUID:UUID
|
this.AUTH_TOKEN:token:token:UUID:UUID
|
||||||
*/
|
*/
|
||||||
private validateRedisKey(redisKey: string, type: string): boolean {
|
private validateRedisKey(redisKey: string): boolean {
|
||||||
if (!redisKey.startsWith(type + ':')) {
|
if (!redisKey.startsWith(this.AUTH_TOKEN + ':')) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
`Invalid Redis key format. Must start with ${type}:`,
|
`Invalid Redis key format. Must start with ${this.AUTH_TOKEN}:`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const colonCount = (redisKey.match(/:/g) || []).length;
|
const colonCount = (redisKey.match(/:/g) || []).length;
|
||||||
|
|
@ -53,7 +53,7 @@ export class RedisHandlers {
|
||||||
throw new ForbiddenException('Access token header is missing');
|
throw new ForbiddenException('Access token header is missing');
|
||||||
}
|
}
|
||||||
const mergedRedisKey = `${this.AUTH_TOKEN}:${acsToken}:${acsToken}:*:*`;
|
const mergedRedisKey = `${this.AUTH_TOKEN}:${acsToken}:${acsToken}:*:*`;
|
||||||
this.validateRedisKey(mergedRedisKey, this.AUTH_TOKEN);
|
this.validateRedisKey(mergedRedisKey);
|
||||||
return mergedRedisKey;
|
return mergedRedisKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,20 +66,8 @@ export class RedisHandlers {
|
||||||
if (!slcToken) {
|
if (!slcToken) {
|
||||||
throw new ForbiddenException('Select token header is missing');
|
throw new ForbiddenException('Select token header is missing');
|
||||||
}
|
}
|
||||||
const mergedRedisKey = `${this.SELECT_TOKEN}:${acsToken}:${slcToken}:*:*`;
|
const mergedRedisKey = `${this.AUTH_TOKEN}:${acsToken}:${slcToken}:*:*`;
|
||||||
this.validateRedisKey(mergedRedisKey, this.SELECT_TOKEN);
|
this.validateRedisKey(mergedRedisKey);
|
||||||
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;
|
return mergedRedisKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,8 +79,8 @@ export class RedisHandlers {
|
||||||
return this.passwordService.generateAccessToken();
|
return this.passwordService.generateAccessToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async scanKeys(pattern: string, type: string): Promise<string[]> {
|
private async scanKeys(pattern: string): Promise<string[]> {
|
||||||
this.validateRedisKey(pattern, type);
|
this.validateRedisKey(pattern);
|
||||||
const client = (this.cacheService as any).client;
|
const client = (this.cacheService as any).client;
|
||||||
if (!client) throw new Error('Redis client not available');
|
if (!client) throw new Error('Redis client not available');
|
||||||
|
|
||||||
|
|
@ -115,7 +103,7 @@ export class RedisHandlers {
|
||||||
async getLoginFromRedis(req: Request): Promise<LoginFromRedis | null> {
|
async getLoginFromRedis(req: Request): Promise<LoginFromRedis | null> {
|
||||||
const mergedKey = this.mergeLoginKey(req);
|
const mergedKey = this.mergeLoginKey(req);
|
||||||
if (mergedKey.includes('*')) {
|
if (mergedKey.includes('*')) {
|
||||||
const keys = await this.scanKeys(mergedKey, this.AUTH_TOKEN);
|
const keys = await this.scanKeys(mergedKey);
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
throw new ForbiddenException('Authorization failed - No matching keys');
|
throw new ForbiddenException('Authorization failed - No matching keys');
|
||||||
}
|
}
|
||||||
|
|
@ -134,15 +122,13 @@ export class RedisHandlers {
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = await this.cacheService.get(mergedKey);
|
const value = await this.cacheService.get(mergedKey);
|
||||||
return value
|
return value ? { key: mergedKey, value } : null;
|
||||||
? { key: mergedKey, value: AuthTokenSchema.parse(value) }
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSelectFromRedis(req: Request): Promise<SelectFromRedis | null> {
|
async getSelectFromRedis(req: Request): Promise<SelectFromRedis | null> {
|
||||||
const mergedKey = this.mergeSelectKey(req);
|
const mergedKey = this.mergeSelectKey(req);
|
||||||
if (mergedKey.includes('*')) {
|
if (mergedKey.includes('*')) {
|
||||||
const keys = await this.scanKeys(mergedKey, this.SELECT_TOKEN);
|
const keys = await this.scanKeys(mergedKey);
|
||||||
|
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
|
|
@ -161,56 +147,7 @@ export class RedisHandlers {
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = await this.cacheService.get(mergedKey);
|
const value = await this.cacheService.get(mergedKey);
|
||||||
return value
|
return value ? { key: mergedKey, value } : null;
|
||||||
? { 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> {
|
async deleteLoginFromRedis(req: Request): Promise<any> {
|
||||||
|
|
@ -249,7 +186,7 @@ export class RedisHandlers {
|
||||||
livingUUID: string,
|
livingUUID: string,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const selectToken = this.generateSelectToken(accessToken, userUUID);
|
const selectToken = this.generateSelectToken(accessToken, userUUID);
|
||||||
const redisKey = `${this.SELECT_TOKEN}:${accessToken}:${selectToken}:${userUUID}:${livingUUID}`;
|
const redisKey = `${this.AUTH_TOKEN}:${accessToken}:${selectToken}:${userUUID}:${livingUUID}`;
|
||||||
await this.cacheService.set_with_ttl(redisKey, token, 60 * 30);
|
await this.cacheService.set_with_ttl(redisKey, token, 60 * 30);
|
||||||
return selectToken;
|
return selectToken;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,31 +16,5 @@
|
||||||
},
|
},
|
||||||
"LocaleLayout": {
|
"LocaleLayout": {
|
||||||
"title": "Next.js i18n Application"
|
"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,31 +16,5 @@
|
||||||
},
|
},
|
||||||
"LocaleLayout": {
|
"LocaleLayout": {
|
||||||
"title": "Next.js i18n Uygulaması"
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,17 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cookies-next": "^6.1.0",
|
|
||||||
"ioredis": "^5.6.1",
|
|
||||||
"lucide-react": "^0.533.0",
|
|
||||||
"next": "15.4.4",
|
"next": "15.4.4",
|
||||||
"next-crypto": "^1.0.8",
|
|
||||||
"next-intl": "^4.3.4",
|
"next-intl": "^4.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0"
|
||||||
"zod": "^4.0.10"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"daisyui": "^5.0.50",
|
"tailwindcss": "^4",
|
||||||
"tailwindcss": "^4.1.11",
|
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -544,12 +538,6 @@
|
||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ioredis/commands": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@isaacs/fs-minipass": {
|
"node_modules/@isaacs/fs-minipass": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||||
|
|
@ -1102,15 +1090,6 @@
|
||||||
"node": ">=6"
|
"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": {
|
"node_modules/color": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
|
|
@ -1156,28 +1135,6 @@
|
||||||
"simple-swizzle": "^0.2.2"
|
"simple-swizzle": "^0.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cookies-next": {
|
|
||||||
"version": "6.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-6.1.0.tgz",
|
|
||||||
"integrity": "sha512-8MqWliHg6YRatqlup5HlKCqXM5cFtwq9BVowDpPniPfbTOmrfIEXUQOcRFVXQltV+hyvKDRGJPNtceICkiJ/IA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cookie": "^1.0.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"next": ">=15.0.0",
|
|
||||||
"react": ">= 16.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
|
@ -1185,48 +1142,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/decimal.js": {
|
||||||
"version": "10.6.0",
|
"version": "10.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
|
|
@ -1270,30 +1191,6 @@
|
||||||
"tslib": "^2.8.0"
|
"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": {
|
"node_modules/is-arrayish": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||||
|
|
@ -1550,27 +1447,6 @@
|
||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash.defaults": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isarguments": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lucide-react": {
|
|
||||||
"version": "0.533.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.533.0.tgz",
|
|
||||||
"integrity": "sha512-XwRo6CQowPRe1cfBJITmHytjR3XS4AZpV6rrBFEzoghARgyU2RK3yNRSnRkSFFSQJWFdQ8f4Wk1awgLLSy1NCQ==",
|
|
||||||
"license": "ISC",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.17",
|
"version": "0.30.17",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
|
|
@ -1620,12 +1496,6 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
|
@ -1705,12 +1575,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"node_modules/next-intl": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.4.tgz",
|
||||||
|
|
@ -1822,27 +1686,6 @@
|
||||||
"react": "^19.1.0"
|
"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": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.26.0",
|
"version": "0.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||||
|
|
@ -1924,12 +1767,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/standard-as-callback": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/styled-jsx": {
|
"node_modules/styled-jsx": {
|
||||||
"version": "5.1.6",
|
"version": "5.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||||
|
|
@ -2038,15 +1875,6 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,23 +10,17 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cookies-next": "^6.1.0",
|
|
||||||
"ioredis": "^5.6.1",
|
|
||||||
"lucide-react": "^0.533.0",
|
|
||||||
"next": "15.4.4",
|
"next": "15.4.4",
|
||||||
"next-crypto": "^1.0.8",
|
|
||||||
"next-intl": "^4.3.4",
|
"next-intl": "^4.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0"
|
||||||
"zod": "^4.0.10"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"daisyui": "^5.0.50",
|
"tailwindcss": "^4",
|
||||||
"tailwindcss": "^4.1.11",
|
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,282 +0,0 @@
|
||||||
'use client';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { apiGetFetcher } from '@/lib/fetcher';
|
|
||||||
|
|
||||||
export default function PageSelect() {
|
|
||||||
const t = useTranslations('Select');
|
|
||||||
const router = useRouter();
|
|
||||||
const [selectionList, setSelectionList] = useState<{ type: string, list: any[] } | null>(null);
|
|
||||||
const [selectedOption, setSelectedOption] = useState<string | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchSelectionList = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
apiGetFetcher({ url: '/api/auth/selections', isNoCache: true }).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
if (res.data && typeof res.data === 'object' && 'type' in res.data && 'list' in res.data) {
|
|
||||||
setSelectionList(res.data as { type: string, list: any[] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching selection list:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchSelectionList();
|
|
||||||
}, []);
|
|
||||||
const handleSelection = (id: string) => { setSelectedOption(id) };
|
|
||||||
const handleContinue = async () => {
|
|
||||||
if (!selectedOption) return;
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
console.log('Selected option:', selectedOption);
|
|
||||||
const payload = { uuid: selectedOption };
|
|
||||||
const response = await fetch('/api/auth/select', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), });
|
|
||||||
const result = await response.json();
|
|
||||||
if (response.ok && result.status === 200) {
|
|
||||||
console.log('Selection successful, redirecting to venue page');
|
|
||||||
router.push('/venue');
|
|
||||||
} else {
|
|
||||||
console.error('Selection failed:', result.message);
|
|
||||||
alert(`Selection failed: ${result.message || 'Unknown error'}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error submitting selection:', error);
|
|
||||||
alert('An error occurred while submitting your selection. Please try again.');
|
|
||||||
} finally { setIsLoading(false) }
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,3 @@
|
||||||
'use server';
|
export default function SelectPage() {
|
||||||
import { Locale } from 'next-intl';
|
return <div></div>;
|
||||||
import { checkAccess, checkSelectionOnSelectPage } from '@/app/api/guards';
|
|
||||||
import SelectPageClient from './SelectPage';
|
|
||||||
|
|
||||||
export default async function PageSelect({ params }: { params: Promise<{ locale: string }> }) {
|
|
||||||
const { locale } = await params;
|
|
||||||
await checkAccess(locale as Locale);
|
|
||||||
await checkSelectionOnSelectPage(locale as Locale);
|
|
||||||
return <SelectPageClient />;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,5 @@
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { headers } from 'next/headers';
|
|
||||||
import { removeLocaleFromPath } from '@/lib/helpers';
|
|
||||||
|
|
||||||
function removeSubStringFromPath(headersList: Headers) {
|
export default function ProtectedLayout({ children }: { children: ReactNode }) {
|
||||||
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}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export default function OfficePage() {
|
export default function OfficePage() {
|
||||||
return <div>Office Page</div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export default function TrialPage() {
|
|
||||||
return <div>TrialPage</div>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export default function VenuePage() {
|
export default function VenuePage() {
|
||||||
return <div>Venue Page</div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,279 +0,0 @@
|
||||||
'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,14 +1,19 @@
|
||||||
import LocaleSwitcherServer from '@/components/LocaleSwitcherServer';
|
// Server Component
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { Link } from '@/i18n/navigation';
|
import { Link } from '@/i18n/navigation';
|
||||||
import { Locale } from '@/i18n/locales';
|
import { Locale } from '@/i18n/locales';
|
||||||
|
import LocaleSwitcherServer from '@/components/LocaleSwitcherServer';
|
||||||
|
|
||||||
|
// Define the props type to get the locale parameter
|
||||||
type Props = {
|
type Props = {
|
||||||
params: Promise<{ locale: string }>;
|
params: Promise<{ locale: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function HomePage({ params }: Props) {
|
export default async function HomePage({ params }: Props) {
|
||||||
|
// Get the locale from params
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
|
|
||||||
|
// Get translations with the correct locale
|
||||||
const t = await getTranslations({ locale: locale as Locale, namespace: 'Index' });
|
const t = await getTranslations({ locale: locale as Locale, namespace: 'Index' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
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" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { doSelect } from "@/fetchers/auth/login/fetch";
|
|
||||||
import { setCookieSelectToken } from "@/fetchers/token/cookies";
|
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { uuid } = await req.json();
|
|
||||||
console.log("Select attempt for UUID:", uuid);
|
|
||||||
|
|
||||||
const response = await doSelect({ uuid });
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
console.log("Select failed with status:", response.status);
|
|
||||||
return NextResponse.json({ status: 401, message: "Select failed" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = response.data as any;
|
|
||||||
const token = data.token;
|
|
||||||
console.log("Select token received:", token ? "[PRESENT]" : "[MISSING]");
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
console.error("No token received from select response");
|
|
||||||
return NextResponse.json({ status: 500, message: "No token received" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the cookie using the server-side utility function
|
|
||||||
await setCookieSelectToken(token);
|
|
||||||
console.log("Select cookie set via setCookieSelectToken");
|
|
||||||
|
|
||||||
// Return the response
|
|
||||||
return NextResponse.json({ status: 200 });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in select route:", error);
|
|
||||||
return NextResponse.json({ status: 500, message: "Internal server error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { getSelectionList } from "@/fetchers/auth/selection/list/fetch";
|
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
|
||||||
return await getSelectionList();
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
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}"` });
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
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}"` });
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
'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,10 +1,4 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tailwindcss";
|
|
||||||
@plugin "daisyui";
|
|
||||||
|
|
||||||
/* @plugin "daisyui" {
|
|
||||||
themes: all;
|
|
||||||
} */
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import LocaleSwitcherClient from '@/components/LocaleSwitcherClient';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const t = useTranslations('Index');
|
const t = useTranslations('Index');
|
||||||
const n = useTranslations('Index.navigation');
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
|
|
@ -20,13 +18,11 @@ export default function HomePage() {
|
||||||
<main>
|
<main>
|
||||||
<h1>{t('title')}</h1>
|
<h1>{t('title')}</h1>
|
||||||
<p>{t('description')}</p>
|
<p>{t('description')}</p>
|
||||||
<p>{n('title')} : {params?.locale || 'tr'}</p>
|
<p>{t('navigation.title')} : {params.locale}</p>
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
<LocaleSwitcherClient />
|
<LocaleSwitcherClient />
|
||||||
<button onClick={() => handleNavigation('/about')}>{n('about')}</button>
|
<button onClick={() => handleNavigation('/about')}>{t('navigation.about')}</button>
|
||||||
<button onClick={() => handleNavigation('/home')}>{n('home')}</button>
|
<button onClick={() => handleNavigation('/home')}>{t('navigation.home')}</button>
|
||||||
<button onClick={() => handleNavigation('/login')}>{n('login')}</button>
|
|
||||||
<button onClick={() => handleNavigation('/select')}>{n('select')}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
'use server';
|
|
||||||
import { fetchData } from "@/fetchers/fecther";
|
|
||||||
import { urlSelectEndpoint, urlLoginEndpoint } from "@/fetchers/urls";
|
|
||||||
import { LoginViaAccessKeys } from "@/fetchers/types/login/validations";
|
|
||||||
import { setCookieAccessToken, setCookieSelectToken } from "@/fetchers/token/cookies";
|
|
||||||
|
|
||||||
|
|
||||||
async function doLogin(payload: LoginViaAccessKeys) {
|
|
||||||
const response = await fetchData(
|
|
||||||
urlLoginEndpoint,
|
|
||||||
payload,
|
|
||||||
"POST",
|
|
||||||
false,
|
|
||||||
5000
|
|
||||||
);
|
|
||||||
if (response.status !== 200) {
|
|
||||||
console.log('doLogin response', response);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doSelect(payload: { uuid: string }) {
|
|
||||||
const response = await fetchData(
|
|
||||||
urlSelectEndpoint,
|
|
||||||
payload,
|
|
||||||
"POST",
|
|
||||||
false,
|
|
||||||
5000
|
|
||||||
);
|
|
||||||
if (response.status == 200) {
|
|
||||||
console.log('doSelect response', response);
|
|
||||||
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { doLogin, doSelect };
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
'use server';
|
|
||||||
import { getAccessObjectField } from "@/fetchers/token/access";
|
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
|
|
||||||
async function getSelectionList() {
|
|
||||||
const selectionList = await getAccessObjectField("selectionList");
|
|
||||||
if (!selectionList) return NextResponse.json({ success: false, message: "Selection list not found" });
|
|
||||||
return NextResponse.json({ success: true, data: selectionList, message: "Selection list fetched successfully" });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export { getSelectionList };
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies";
|
|
||||||
import NextCrypto from "next-crypto";
|
|
||||||
|
|
||||||
const tokenSecretEnv = process.env.TOKENSECRET_90;
|
|
||||||
const tokenSecret = tokenSecretEnv || "e781d1b0-9418-40b3-9940-385abf81a0b7";
|
|
||||||
const REDIS_TIMEOUT = 5000;
|
|
||||||
const nextCrypto = new NextCrypto(tokenSecret);
|
|
||||||
|
|
||||||
// Cookie options for cookies-next
|
|
||||||
const cookieOptions = {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: false,
|
|
||||||
sameSite: "lax" as const,
|
|
||||||
path: "/",
|
|
||||||
maxAge: 30 * 60, // 30 minutes
|
|
||||||
// priority: "high",
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT: number = 10000;
|
|
||||||
const defaultHeaders: Record<string, string> = {
|
|
||||||
accept: "application/json",
|
|
||||||
language: "tr",
|
|
||||||
domain: "evyos.com.tr",
|
|
||||||
tz: "GMT+3",
|
|
||||||
"Content-type": "application/json",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cookie options for next/headers cookies
|
|
||||||
// const cookieObject: Partial<ResponseCookie> = {
|
|
||||||
// httpOnly: true,
|
|
||||||
// path: "/",
|
|
||||||
// sameSite: "none",
|
|
||||||
// secure: true,
|
|
||||||
// maxAge: 1800,
|
|
||||||
// priority: "high",
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const DEFAULT_RESPONSE: ApiResponse = {
|
|
||||||
// error: "Hata tipi belirtilmedi",
|
|
||||||
// status: 500,
|
|
||||||
// data: {},
|
|
||||||
// };
|
|
||||||
|
|
||||||
export {
|
|
||||||
DEFAULT_TIMEOUT,
|
|
||||||
// DEFAULT_RESPONSE,
|
|
||||||
REDIS_TIMEOUT,
|
|
||||||
defaultHeaders,
|
|
||||||
tokenSecret,
|
|
||||||
cookieOptions,
|
|
||||||
nextCrypto,
|
|
||||||
};
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
"use server";
|
|
||||||
import { defaultHeaders, DEFAULT_TIMEOUT } from "./base";
|
|
||||||
import { FetchOptions, HttpMethod, ApiResponse } from "./types";
|
|
||||||
import { getPlainAccessToken } from "./token/access";
|
|
||||||
import { getPlainSelectToken } from "./token/select";
|
|
||||||
|
|
||||||
const DEFAULT_RESPONSE = {
|
|
||||||
error: "Hata tipi belirtilmedi",
|
|
||||||
status: 500,
|
|
||||||
data: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a promise that rejects after a specified timeout
|
|
||||||
* @param ms Timeout in milliseconds
|
|
||||||
* @param controller AbortController to abort the fetch request
|
|
||||||
* @returns A promise that rejects after the timeout
|
|
||||||
*/
|
|
||||||
const createTimeoutPromise = (
|
|
||||||
ms: number,
|
|
||||||
controller: AbortController
|
|
||||||
): Promise<never> => {
|
|
||||||
return new Promise((_, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
controller.abort();
|
|
||||||
reject(new Error(`Request timed out after ${ms}ms`));
|
|
||||||
}, ms);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core fetch function with timeout and error handling
|
|
||||||
* @param url The URL to fetch
|
|
||||||
* @param options Fetch options
|
|
||||||
* @param headers Request headers
|
|
||||||
* @param payload Request payload
|
|
||||||
* @returns API response
|
|
||||||
*/
|
|
||||||
async function coreFetch<T>(
|
|
||||||
url: string,
|
|
||||||
options: FetchOptions = {},
|
|
||||||
headers: Record<string, string> = defaultHeaders,
|
|
||||||
payload?: any
|
|
||||||
): Promise<ApiResponse<T>> {
|
|
||||||
const { method = "POST", cache = false, timeout = DEFAULT_TIMEOUT } = options;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
cache: cache ? "force-cache" : "no-cache",
|
|
||||||
signal: controller.signal,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (method !== "GET" && payload) {
|
|
||||||
fetchOptions.body = JSON.stringify(
|
|
||||||
payload.payload ? payload.payload : payload
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const timeoutPromise = createTimeoutPromise(timeout, controller);
|
|
||||||
const response = await Promise.race([
|
|
||||||
fetch(url, fetchOptions),
|
|
||||||
timeoutPromise,
|
|
||||||
]);
|
|
||||||
const responseData = await response.json();
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
data: responseData || ({} as T),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`API Error (${url}):`, error);
|
|
||||||
return {
|
|
||||||
...DEFAULT_RESPONSE,
|
|
||||||
error: error instanceof Error ? error.message : "Network error",
|
|
||||||
} as ApiResponse<T>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch data without authentication
|
|
||||||
*/
|
|
||||||
async function fetchData<T>(
|
|
||||||
endpoint: string,
|
|
||||||
payload?: any,
|
|
||||||
method: HttpMethod = "POST",
|
|
||||||
cache: boolean = false,
|
|
||||||
timeout: number = DEFAULT_TIMEOUT
|
|
||||||
): Promise<ApiResponse<T>> {
|
|
||||||
return coreFetch<T>(
|
|
||||||
endpoint,
|
|
||||||
{ method, cache, timeout },
|
|
||||||
defaultHeaders,
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch data with authentication token
|
|
||||||
*/
|
|
||||||
async function fetchDataWithToken<T>(
|
|
||||||
endpoint: string,
|
|
||||||
payload?: any,
|
|
||||||
method: HttpMethod = "POST",
|
|
||||||
cache: boolean = false,
|
|
||||||
timeout: number = DEFAULT_TIMEOUT
|
|
||||||
): Promise<ApiResponse<T>> {
|
|
||||||
const accessToken = await getPlainAccessToken();
|
|
||||||
const selectToken = await getPlainSelectToken();
|
|
||||||
const headers = { ...defaultHeaders, acs: accessToken, slc: selectToken };
|
|
||||||
return coreFetch<T>(endpoint, { method, cache, timeout }, headers, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update data with authentication token and UUID
|
|
||||||
*/
|
|
||||||
async function updateDataWithToken<T>(
|
|
||||||
endpoint: string,
|
|
||||||
uuid: string,
|
|
||||||
payload?: any,
|
|
||||||
method: HttpMethod = "POST",
|
|
||||||
cache: boolean = false,
|
|
||||||
timeout: number = DEFAULT_TIMEOUT
|
|
||||||
): Promise<ApiResponse<T>> {
|
|
||||||
const accessToken = await getPlainAccessToken();
|
|
||||||
const selectToken = await getPlainSelectToken();
|
|
||||||
const headers = { ...defaultHeaders, acs: accessToken, slc: selectToken };
|
|
||||||
return coreFetch<T>(
|
|
||||||
`${endpoint}/${uuid}`,
|
|
||||||
{ method, cache, timeout },
|
|
||||||
headers,
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { fetchData, fetchDataWithToken, updateDataWithToken };
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import Redis from "ioredis";
|
|
||||||
|
|
||||||
const redis = new Redis({
|
|
||||||
host: process.env.REDIS_HOST,
|
|
||||||
port: parseInt(process.env.REDIS_PORT || "6379", 10),
|
|
||||||
password: process.env.REDIS_PASSWORD || "",
|
|
||||||
db: parseInt(process.env.REDIS_DB || "0", 10),
|
|
||||||
connectTimeout: 5000,
|
|
||||||
maxRetriesPerRequest: 2,
|
|
||||||
retryStrategy: (times) => Math.min(times * 50, 2000),
|
|
||||||
reconnectOnError: (err) => err.message.includes("READONLY"),
|
|
||||||
});
|
|
||||||
|
|
||||||
redis.on("connect", () => console.log("[redis] Connected"));
|
|
||||||
redis.on("error", (err) => console.error("[redis] Error:", err));
|
|
||||||
|
|
||||||
export default redis;
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
import redis from "./redis";
|
|
||||||
import { redisScanAccess, redisScanSelect } from "../types/base";
|
|
||||||
|
|
||||||
interface SScanParams {
|
|
||||||
rKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DScanParams {
|
|
||||||
rKey: string;
|
|
||||||
sKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SetParams<T> {
|
|
||||||
key: string;
|
|
||||||
value: T;
|
|
||||||
ttlSeconds?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetParams {
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateParams<T> {
|
|
||||||
key: string;
|
|
||||||
value: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateFieldParams<T> {
|
|
||||||
key: string;
|
|
||||||
field: string;
|
|
||||||
value: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
type RScan = Promise<string | null>;
|
|
||||||
type RExists = Promise<boolean>;
|
|
||||||
type RUpdate = Promise<void>;
|
|
||||||
type RDelete = Promise<void>;
|
|
||||||
type RSet = Promise<void>;
|
|
||||||
type RJSON<T> = Promise<T | null>;
|
|
||||||
|
|
||||||
export async function setJSON<T>(params: SetParams<T>): RSet {
|
|
||||||
const val = JSON.stringify(params.value);
|
|
||||||
if (params.ttlSeconds) {
|
|
||||||
await redis.set(params.key, val, "EX", params.ttlSeconds);
|
|
||||||
} else {
|
|
||||||
await redis.set(params.key, val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getJSON<T>(params: GetParams): RJSON<T> {
|
|
||||||
const val = await redis.get(params.key);
|
|
||||||
if (!val) return null;
|
|
||||||
try {
|
|
||||||
return JSON.parse(val);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function exists(params: GetParams): RExists {
|
|
||||||
const result = await redis.exists(params.key);
|
|
||||||
return result === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateJSON<T>(params: UpdateParams<T>): RUpdate {
|
|
||||||
const keyExists = await redis.exists(params.key);
|
|
||||||
if (!keyExists) throw new Error("Key not found");
|
|
||||||
await redis.set(params.key, JSON.stringify(params.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateField<T>(params: UpdateFieldParams<T>): RExists {
|
|
||||||
const isExists = await exists({ key: params.key });
|
|
||||||
if (!isExists) {
|
|
||||||
await setJSON({
|
|
||||||
key: params.key,
|
|
||||||
value: { [params.field as string]: params.value },
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const valueFromRedis = await getJSON({ key: params.key });
|
|
||||||
if (!valueFromRedis) throw new Error("Key not found");
|
|
||||||
await updateJSON({
|
|
||||||
key: params.key,
|
|
||||||
value: { ...valueFromRedis, [params.field as string]: params.value },
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteKey(params: GetParams): RDelete {
|
|
||||||
await redis.del(params.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function scanByRKeySingle(params: SScanParams): RScan {
|
|
||||||
const pattern = redisScanAccess(params.rKey);
|
|
||||||
const keys: string[] = [];
|
|
||||||
let cursor = "0";
|
|
||||||
do {
|
|
||||||
const [nextCursor, matchedKeys] = await redis.scan(
|
|
||||||
cursor,
|
|
||||||
"MATCH",
|
|
||||||
pattern
|
|
||||||
);
|
|
||||||
cursor = nextCursor;
|
|
||||||
keys.push(...matchedKeys);
|
|
||||||
} while (cursor !== "0");
|
|
||||||
return keys.length > 0 ? keys[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function scanByRKeyDouble(params: DScanParams): RScan {
|
|
||||||
const pattern = redisScanSelect(params.rKey, params.sKey);
|
|
||||||
console.log("pattern", pattern);
|
|
||||||
const keys: string[] = [];
|
|
||||||
let cursor = "0";
|
|
||||||
do {
|
|
||||||
const [nextCursor, matchedKeys] = await redis.scan(
|
|
||||||
cursor,
|
|
||||||
"MATCH",
|
|
||||||
pattern
|
|
||||||
);
|
|
||||||
cursor = nextCursor;
|
|
||||||
keys.push(...matchedKeys);
|
|
||||||
} while (cursor !== "0");
|
|
||||||
return keys.length > 0 ? keys[0] : null;
|
|
||||||
}
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
'use server';
|
|
||||||
import { AuthError } from "@/fetchers/types/base";
|
|
||||||
import { scanByRKeySingle, getJSON } from "@/fetchers/redis/redisService";
|
|
||||||
import { nextCrypto } from "@/fetchers/base";
|
|
||||||
import { getCookieAccessToken, removeCookieTokens, setCookieAccessToken } from "./cookies";
|
|
||||||
|
|
||||||
async function getPlainAccessToken() {
|
|
||||||
try {
|
|
||||||
let encryptedAccessToken = "";
|
|
||||||
const accessCookie = await getCookieAccessToken();
|
|
||||||
if (accessCookie?.value) {
|
|
||||||
encryptedAccessToken = accessCookie.value;
|
|
||||||
}
|
|
||||||
if (!encryptedAccessToken) {
|
|
||||||
throw new AuthError("No access token found");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const decryptedAccessToken = await nextCrypto.decrypt(encryptedAccessToken) || "";
|
|
||||||
if (!decryptedAccessToken) {
|
|
||||||
throw new AuthError("Access token is invalid");
|
|
||||||
}
|
|
||||||
return decryptedAccessToken;
|
|
||||||
} catch (decryptError) {
|
|
||||||
throw new AuthError("Failed to decrypt access token");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
throw new AuthError("No access token found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAccessToken() {
|
|
||||||
try {
|
|
||||||
const plainAccessToken = await getPlainAccessToken();
|
|
||||||
if (!plainAccessToken) {
|
|
||||||
throw new AuthError("No access token found");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const scanToken = await scanByRKeySingle({ rKey: plainAccessToken });
|
|
||||||
if (!scanToken) throw new AuthError("Access token is invalid");
|
|
||||||
return scanToken;
|
|
||||||
} catch (scanError) {
|
|
||||||
throw new AuthError("Failed to validate access token");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
throw new AuthError("No access token found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAccessObject() {
|
|
||||||
try {
|
|
||||||
const accessToken = await getAccessToken();
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new AuthError("No access token found");
|
|
||||||
}
|
|
||||||
const accessObject = await getJSON({ key: accessToken });
|
|
||||||
return accessObject;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
throw new AuthError("No access token found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAccessObjectField(field: string): Promise<Record<string, any> | null> {
|
|
||||||
try {
|
|
||||||
const accessToken = await getAccessToken();
|
|
||||||
const accessObject = await getJSON({ key: accessToken });
|
|
||||||
if (!accessObject) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
if (Object.keys(accessObject).includes(field)) { return accessObject[field as keyof typeof accessObject] }
|
|
||||||
}
|
|
||||||
} catch (error) { }
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isAccessTokenValid() {
|
|
||||||
try {
|
|
||||||
const accessToken = await getAccessObject();
|
|
||||||
console.log('accessToken', accessToken);
|
|
||||||
if (accessToken) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
removeCookieTokens();
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} catch (error) { }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setAccessToken(token: string) {
|
|
||||||
await setCookieAccessToken(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeAccessToken() {
|
|
||||||
await removeCookieTokens();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getAccessToken, setAccessToken, removeAccessToken, getPlainAccessToken, isAccessTokenValid, getAccessObject, getAccessObjectField };
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
'use server';
|
|
||||||
import { nextCrypto, cookieOptions } from "@/fetchers/base";
|
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
|
||||||
const ACCESS_TOKEN_COOKIE = 'acs';
|
|
||||||
const SELECT_TOKEN_COOKIE = 'slc';
|
|
||||||
|
|
||||||
export async function getCookieAccessToken() {
|
|
||||||
try {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
const encryptedToken = cookieStore.get(ACCESS_TOKEN_COOKIE);
|
|
||||||
if (!encryptedToken) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return { name: ACCESS_TOKEN_COOKIE, value: encryptedToken.value };
|
|
||||||
} catch (error) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setCookieAccessToken(token: string) {
|
|
||||||
try {
|
|
||||||
const encryptedToken = await nextCrypto.encrypt(token);
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
cookieStore.set(ACCESS_TOKEN_COOKIE, encryptedToken, {
|
|
||||||
httpOnly: cookieOptions.httpOnly,
|
|
||||||
secure: cookieOptions.secure,
|
|
||||||
sameSite: cookieOptions.sameSite,
|
|
||||||
path: cookieOptions.path,
|
|
||||||
maxAge: cookieOptions.maxAge
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCookieSelectToken() {
|
|
||||||
try {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
const encryptedToken = cookieStore.get(SELECT_TOKEN_COOKIE);
|
|
||||||
if (!encryptedToken) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return { name: SELECT_TOKEN_COOKIE, value: encryptedToken.value };
|
|
||||||
} catch (error) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setCookieSelectToken(token: string) {
|
|
||||||
try {
|
|
||||||
const encryptedToken = await nextCrypto.encrypt(token);
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
cookieStore.set(SELECT_TOKEN_COOKIE, encryptedToken, {
|
|
||||||
httpOnly: cookieOptions.httpOnly,
|
|
||||||
secure: cookieOptions.secure,
|
|
||||||
sameSite: cookieOptions.sameSite,
|
|
||||||
path: cookieOptions.path,
|
|
||||||
maxAge: cookieOptions.maxAge
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeAccessToken() {
|
|
||||||
try {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
cookieStore.delete(ACCESS_TOKEN_COOKIE);
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeSelectToken() {
|
|
||||||
try {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
cookieStore.delete(SELECT_TOKEN_COOKIE);
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeCookieTokens() {
|
|
||||||
try {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
cookieStore.delete(ACCESS_TOKEN_COOKIE);
|
|
||||||
cookieStore.delete(SELECT_TOKEN_COOKIE);
|
|
||||||
} catch (error) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
'use server';
|
|
||||||
import { AuthError } from "@/fetchers/types/base";
|
|
||||||
import { scanByRKeyDouble } from "@/fetchers/redis/redisService";
|
|
||||||
import { getPlainAccessToken } from "./access";
|
|
||||||
import { nextCrypto } from "@/fetchers/base";
|
|
||||||
import { getCookieSelectToken, removeCookieTokens, setCookieSelectToken } from "./cookies";
|
|
||||||
|
|
||||||
async function getPlainSelectToken() {
|
|
||||||
console.log('getPlainSelectToken is triggered...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
let encryptedSelectToken = "";
|
|
||||||
|
|
||||||
// Try to get token from cookies first
|
|
||||||
const selectCookie = await getCookieSelectToken();
|
|
||||||
if (selectCookie?.value) {
|
|
||||||
encryptedSelectToken = selectCookie.value;
|
|
||||||
console.log('Select token from cookie:', 'found');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('getPlainSelectToken encryptedSelectToken', encryptedSelectToken ? `${encryptedSelectToken.substring(0, 10)}...` : 'empty');
|
|
||||||
|
|
||||||
if (!encryptedSelectToken) {
|
|
||||||
console.log('No select token found');
|
|
||||||
throw new AuthError("No select token found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptedSelectToken = await nextCrypto.decrypt(encryptedSelectToken) || "";
|
|
||||||
console.log('getPlainSelectToken decryptedSelectToken', decryptedSelectToken ? `${decryptedSelectToken.substring(0, 10)}...` : 'empty');
|
|
||||||
|
|
||||||
if (!decryptedSelectToken) {
|
|
||||||
console.log('Decrypted select token is invalid or empty');
|
|
||||||
throw new AuthError("Select token is invalid");
|
|
||||||
}
|
|
||||||
|
|
||||||
return decryptedSelectToken;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Error in getPlainSelectToken:', error);
|
|
||||||
throw new AuthError("No select token found on cookies");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSelectToken() {
|
|
||||||
try {
|
|
||||||
const plainAccessToken = await getPlainAccessToken();
|
|
||||||
const plainSelectToken = await getPlainSelectToken();
|
|
||||||
console.log('plainAccessToken', plainAccessToken);
|
|
||||||
console.log('plainSelectToken', plainSelectToken);
|
|
||||||
const scanToken = await scanByRKeyDouble({ rKey: plainAccessToken, sKey: plainSelectToken });
|
|
||||||
if (!scanToken) throw new AuthError("Select token is invalid");
|
|
||||||
return scanToken;
|
|
||||||
}
|
|
||||||
catch (error) { throw new AuthError("No select token found in headers") }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setSelectToken(token: string) {
|
|
||||||
console.log('setSelectToken is triggered...');
|
|
||||||
await setCookieSelectToken(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isSelectTokenValid() {
|
|
||||||
try {
|
|
||||||
const selectToken = await getSelectToken();
|
|
||||||
console.log('isSelectTokenValid selectToken', selectToken);
|
|
||||||
if (selectToken) return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.log('isSelectTokenValid error', error);
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeSelectToken() {
|
|
||||||
console.log('removeSelectToken is triggered');
|
|
||||||
await removeCookieTokens();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getSelectToken, setSelectToken, removeSelectToken, getPlainSelectToken, isSelectTokenValid };
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
||||||
|
|
||||||
interface ApiResponse<T = any> {
|
|
||||||
status: number;
|
|
||||||
data: T;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FetchOptions {
|
|
||||||
method?: HttpMethod;
|
|
||||||
cache?: boolean;
|
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CookieObject {
|
|
||||||
httpOnly: boolean;
|
|
||||||
path: string;
|
|
||||||
sameSite: string;
|
|
||||||
secure: boolean;
|
|
||||||
maxAge: number;
|
|
||||||
priority: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Pagination {
|
|
||||||
page?: number;
|
|
||||||
size?: number;
|
|
||||||
orderField?: string[];
|
|
||||||
orderType?: string[];
|
|
||||||
query?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { HttpMethod, ApiResponse, FetchOptions, CookieObject, Pagination };
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
export class AuthError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = "AuthError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessExt = "AUTH_TOKEN";
|
|
||||||
const selectExt = "SELECT_TOKEN";
|
|
||||||
function redisScanAccess(cookieToken: string) {
|
|
||||||
return `${accessExt}:${cookieToken}:${cookieToken}:*:*`;
|
|
||||||
}
|
|
||||||
function redisScanSelect(cookieToken: string, selectToken: string) {
|
|
||||||
return `${selectExt}:${cookieToken}:${selectToken}:*:*`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { redisScanAccess, redisScanSelect };
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
interface LoginViaAccessKeys {
|
|
||||||
accessKey: string;
|
|
||||||
password: string;
|
|
||||||
rememberMe: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoginSelect {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { LoginViaAccessKeys, LoginSelect };
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
const baseUrl = process.env.API_URL || "http://localhost:8001";
|
|
||||||
const selfApi = process.env.SELF_API_URL || "http://localhost:3000";
|
|
||||||
const urlLoginEndpoint = `${baseUrl}/auth/login`;
|
|
||||||
const urlSelectEndpoint = `${baseUrl}/auth/select`;
|
|
||||||
|
|
||||||
export { urlLoginEndpoint, urlSelectEndpoint, selfApi };
|
|
||||||
|
|
@ -12,83 +12,10 @@
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"current": "Current Language",
|
"current": "Current Language"
|
||||||
"login": "Login",
|
|
||||||
"select": "Select"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"LocaleLayout": {
|
"LocaleLayout": {
|
||||||
"title": "My Application"
|
"title": "My Application"
|
||||||
},
|
|
||||||
"Login": {
|
|
||||||
"title": "Login",
|
|
||||||
"description": "Login to your account",
|
|
||||||
"email": "Your email",
|
|
||||||
"password": "Your password",
|
|
||||||
"login": "Log in",
|
|
||||||
"register": "Register",
|
|
||||||
"createAccount": "Create an account",
|
|
||||||
"forgotPassword": "Forgot your password?",
|
|
||||||
"recentLogins": "Recent logins",
|
|
||||||
"clickPictureOrAdd": "Click your picture or add an account",
|
|
||||||
"addAccount": "Add an account",
|
|
||||||
"show": "Show",
|
|
||||||
"hide": "Hide",
|
|
||||||
"signInWithGoogle": "Sign in with Google",
|
|
||||||
"signInWithFacebook": "Sign in with Facebook",
|
|
||||||
"enterCredentials": "Enter your credentials to continue",
|
|
||||||
"rememberMe": "Remember me",
|
|
||||||
"orContinueWith": "or continue with",
|
|
||||||
"noAccount": "Don't have an account?",
|
|
||||||
"signUp": "Sign up",
|
|
||||||
"copyright": "© 2023 Your Company. All rights reserved.",
|
|
||||||
"welcomeBack": "Welcome Back",
|
|
||||||
"continueJourney": "Sign in to continue your journey with us",
|
|
||||||
"emailWrong": "Please check email format",
|
|
||||||
"passwordWrong": "Please check password format"
|
|
||||||
},
|
|
||||||
"Select": {
|
|
||||||
"title": "Select Your Option",
|
|
||||||
"description": "Please select one of the following options to continue",
|
|
||||||
"employee": "Employee",
|
|
||||||
"occupant_type": "Occupant Type",
|
|
||||||
"staff": "Staff",
|
|
||||||
"uuid": "UUID",
|
|
||||||
"department": "Department",
|
|
||||||
"name": "Name",
|
|
||||||
"code": "Code",
|
|
||||||
"company": "Company",
|
|
||||||
"occupant": "Occupant",
|
|
||||||
"occupant_code": "Occupant Code",
|
|
||||||
"building": "Building",
|
|
||||||
"type": "Type",
|
|
||||||
"part_details": "Part Details",
|
|
||||||
"no": "No",
|
|
||||||
"level": "Level",
|
|
||||||
"status": "Status",
|
|
||||||
"livable": "Livable",
|
|
||||||
"not_livable": "Not Livable",
|
|
||||||
"selection": "Selection",
|
|
||||||
"id": "ID",
|
|
||||||
"processing": "Processing...",
|
|
||||||
"continue": "Continue",
|
|
||||||
"select_option": "Select an option to continue",
|
|
||||||
"recentLogins": "Recent logins",
|
|
||||||
"clickPictureOrAdd": "Click your picture or add an account",
|
|
||||||
"addAccount": "Add an account",
|
|
||||||
"show": "Show",
|
|
||||||
"hide": "Hide",
|
|
||||||
"signInWithGoogle": "Sign in with Google",
|
|
||||||
"signInWithFacebook": "Sign in with Facebook",
|
|
||||||
"enterCredentials": "Enter your credentials to continue",
|
|
||||||
"rememberMe": "Remember me",
|
|
||||||
"orContinueWith": "or continue with",
|
|
||||||
"noAccount": "Don't have an account?",
|
|
||||||
"signUp": "Sign up",
|
|
||||||
"copyright": "© 2023 Your Company. All rights reserved.",
|
|
||||||
"welcomeBack": "Welcome Back",
|
|
||||||
"continueJourney": "Sign in to continue your journey with us",
|
|
||||||
"emailWrong": "Please check email format",
|
|
||||||
"passwordWrong": "Please check password format"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ export const usePathname = () => {
|
||||||
const locale = params?.locale as Locale;
|
const locale = params?.locale as Locale;
|
||||||
|
|
||||||
// Remove locale prefix from pathname
|
// Remove locale prefix from pathname
|
||||||
if (locale && nextPathname && nextPathname.startsWith(`/${locale}`)) {
|
if (locale && nextPathname.startsWith(`/${locale}`)) {
|
||||||
return nextPathname.substring(`/${locale}`.length) || "/";
|
return nextPathname.substring(`/${locale}`.length) || "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,83 +12,10 @@
|
||||||
"home": "Ana Sayfa",
|
"home": "Ana Sayfa",
|
||||||
"about": "Hakkında",
|
"about": "Hakkında",
|
||||||
"contact": "İletişim",
|
"contact": "İletişim",
|
||||||
"current": "Mevcut Dili",
|
"current": "Mevcut Dili"
|
||||||
"login": "Giriş Yap",
|
|
||||||
"select": "Görev Seç"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"LocaleLayout": {
|
"LocaleLayout": {
|
||||||
"title": "Uygulamam"
|
"title": "Uygulamam"
|
||||||
},
|
|
||||||
"Login": {
|
|
||||||
"title": "Giriş Yap",
|
|
||||||
"description": "Hesabınıza giriş yap",
|
|
||||||
"email": "E-posta adresiniz",
|
|
||||||
"password": "Parolanız",
|
|
||||||
"login": "Giriş yap",
|
|
||||||
"register": "Kayıt Ol",
|
|
||||||
"createAccount": "Hesap oluştur",
|
|
||||||
"forgotPassword": "Parolanızı mı unuttunuz?",
|
|
||||||
"recentLogins": "Son girişler",
|
|
||||||
"clickPictureOrAdd": "Resminize tıklayın veya hesap ekleyin",
|
|
||||||
"addAccount": "Hesap ekle",
|
|
||||||
"show": "Göster",
|
|
||||||
"hide": "Gizle",
|
|
||||||
"signInWithGoogle": "Google ile giriş yap",
|
|
||||||
"signInWithFacebook": "Facebook ile giriş yap",
|
|
||||||
"enterCredentials": "Devam etmek için bilgilerinizi girin",
|
|
||||||
"rememberMe": "Beni hatırla",
|
|
||||||
"orContinueWith": "veya şununla devam et",
|
|
||||||
"noAccount": "Hesabınız yok mu?",
|
|
||||||
"signUp": "Kayıt ol",
|
|
||||||
"copyright": "© 2023 Şirketiniz. Tüm hakları saklıdır.",
|
|
||||||
"welcomeBack": "Tekrar Hoşgeldiniz",
|
|
||||||
"continueJourney": "Yolculuğunuza devam etmek için giriş yapın",
|
|
||||||
"emailWrong": "Emaili tekrar kontrol edin",
|
|
||||||
"passwordWrong": "Parolayı tekrar kontrol edin"
|
|
||||||
},
|
|
||||||
"Select": {
|
|
||||||
"title": "Seçeneğinizi Seçin",
|
|
||||||
"description": "Devam etmek için lütfen aşağıdaki seçeneklerden birini seçin",
|
|
||||||
"employee": "Çalışan",
|
|
||||||
"staff": "Personel",
|
|
||||||
"occupant_type": "İkamet Tipi",
|
|
||||||
"uuid": "UUID",
|
|
||||||
"department": "Departman",
|
|
||||||
"name": "İsim",
|
|
||||||
"code": "Kod",
|
|
||||||
"company": "Şirket",
|
|
||||||
"occupant": "Oturak",
|
|
||||||
"occupant_code": "Oturak Kodu",
|
|
||||||
"building": "Bina",
|
|
||||||
"type": "Tip",
|
|
||||||
"part_details": "Parça Detayları",
|
|
||||||
"no": "No",
|
|
||||||
"level": "Seviye",
|
|
||||||
"status": "Durum",
|
|
||||||
"livable": "Yaşanabilir",
|
|
||||||
"not_livable": "Yaşanamaz",
|
|
||||||
"selection": "Seçim",
|
|
||||||
"id": "ID",
|
|
||||||
"processing": "İşleniyor...",
|
|
||||||
"continue": "Devam Et",
|
|
||||||
"select_option": "Devam etmek için bir seçenek seçin",
|
|
||||||
"recentLogins": "Son girişler",
|
|
||||||
"clickPictureOrAdd": "Resminize tıklayın veya bir hesap ekleyin",
|
|
||||||
"addAccount": "Hesap ekle",
|
|
||||||
"show": "Göster",
|
|
||||||
"hide": "Gizle",
|
|
||||||
"signInWithGoogle": "Google ile giriş yap",
|
|
||||||
"signInWithFacebook": "Facebook ile giriş yap",
|
|
||||||
"enterCredentials": "Devam etmek için kimlik bilgilerinizi girin",
|
|
||||||
"rememberMe": "Beni hatırla",
|
|
||||||
"orContinueWith": "veya şununla devam et",
|
|
||||||
"noAccount": "Hesabınız yok mu?",
|
|
||||||
"signUp": "Kayıt ol",
|
|
||||||
"copyright": " 2023 Şirketiniz. Tüm hakları saklıdır.",
|
|
||||||
"welcomeBack": "Tekrar Hoş Geldiniz",
|
|
||||||
"continueJourney": "Bizimle yolculuğunuza devam etmek için giriş yapın",
|
|
||||||
"emailWrong": "Lütfen e-posta formatını kontrol edin",
|
|
||||||
"passwordWrong": "Lütfen şifre formatını kontrol edin"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
interface FetcherRequest {
|
|
||||||
url: string;
|
|
||||||
isNoCache: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PostFetcherRequest<T> extends FetcherRequest {
|
|
||||||
body: Record<string, T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetFetcherRequest extends FetcherRequest {
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteFetcherRequest extends GetFetcherRequest { }
|
|
||||||
interface PutFetcherRequest<T> extends PostFetcherRequest<T> { }
|
|
||||||
interface PatchFetcherRequest<T> extends PostFetcherRequest<T> { }
|
|
||||||
|
|
||||||
interface FetcherRespose {
|
|
||||||
success: boolean;
|
|
||||||
}
|
|
||||||
interface PaginationResponse {
|
|
||||||
onPage: number;
|
|
||||||
onPageCount: number;
|
|
||||||
totalPage: number;
|
|
||||||
totalCount: number;
|
|
||||||
next: boolean;
|
|
||||||
back: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FetcherDataResponse<T> extends FetcherRespose {
|
|
||||||
data: Record<string, T> | null;
|
|
||||||
pagination?: PaginationResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiPostFetcher<T>({
|
|
||||||
url,
|
|
||||||
isNoCache,
|
|
||||||
body,
|
|
||||||
}: PostFetcherRequest<T>): Promise<FetcherDataResponse<T>> {
|
|
||||||
try {
|
|
||||||
let headers: Record<string, string> = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"no-cache": isNoCache ? "true" : "false"
|
|
||||||
};
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
credentials: 'include'
|
|
||||||
})
|
|
||||||
if (response.status === 200) {
|
|
||||||
const result = await response.json();
|
|
||||||
return { success: true, data: result.data, pagination: result.pagination }
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('API Post Fetcher error:', error);
|
|
||||||
}
|
|
||||||
return { success: false, data: null }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function apiGetFetcher<T>({
|
|
||||||
url,
|
|
||||||
isNoCache,
|
|
||||||
}: GetFetcherRequest): Promise<FetcherDataResponse<T>> {
|
|
||||||
try {
|
|
||||||
let headers: Record<string, string> = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"no-cache": isNoCache ? "true" : "false"
|
|
||||||
};
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers,
|
|
||||||
credentials: 'include'
|
|
||||||
})
|
|
||||||
let responseText;
|
|
||||||
try {
|
|
||||||
responseText = await response.text();
|
|
||||||
console.log("apiGetFetcher status", response.status, responseText.substring(0, 100));
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error reading response text", e);
|
|
||||||
}
|
|
||||||
if (response.status === 200) {
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
if (responseText) {
|
|
||||||
result = JSON.parse(responseText);
|
|
||||||
return { success: true, data: result.data };
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error parsing JSON response", e);
|
|
||||||
}
|
|
||||||
return { success: false, data: null };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('API Get Fetcher error:', error);
|
|
||||||
}
|
|
||||||
return { success: false, data: null }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function apiDeleteFetcher<T>({
|
|
||||||
url,
|
|
||||||
isNoCache,
|
|
||||||
}: DeleteFetcherRequest): Promise<FetcherDataResponse<T>> {
|
|
||||||
try {
|
|
||||||
let headers: Record<string, string> = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"no-cache": isNoCache ? "true" : "false"
|
|
||||||
};
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers,
|
|
||||||
credentials: 'include'
|
|
||||||
})
|
|
||||||
if (response.status === 200) {
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
return { success: true, data: result.data }
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('API Delete Fetcher error:', error);
|
|
||||||
}
|
|
||||||
return { success: false, data: null }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function apiPutFetcher<T>({
|
|
||||||
url,
|
|
||||||
isNoCache,
|
|
||||||
body,
|
|
||||||
}: PutFetcherRequest<T>): Promise<FetcherDataResponse<T>> {
|
|
||||||
try {
|
|
||||||
let headers: Record<string, string> = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"no-cache": isNoCache ? "true" : "false"
|
|
||||||
};
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "PUT",
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
credentials: 'include'
|
|
||||||
})
|
|
||||||
if (response.status === 200) {
|
|
||||||
const result = await response.json();
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: result.data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) { }
|
|
||||||
return { success: false, data: null }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function apiPatchFetcher<T>({
|
|
||||||
url,
|
|
||||||
isNoCache,
|
|
||||||
body,
|
|
||||||
}: PatchFetcherRequest<T>): Promise<FetcherDataResponse<T>> {
|
|
||||||
try {
|
|
||||||
let headers: Record<string, string> = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"no-cache": isNoCache ? "true" : "false"
|
|
||||||
};
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
credentials: 'include'
|
|
||||||
})
|
|
||||||
if (response.status === 200) {
|
|
||||||
const result = await response.json();
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: result.data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) { }
|
|
||||||
return { success: false, data: null }
|
|
||||||
}
|
|
||||||
|
|
||||||
export { apiPostFetcher, apiGetFetcher, apiDeleteFetcher, apiPutFetcher, apiPatchFetcher };
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import crypto from 'crypto';
|
|
||||||
|
|
||||||
export const buildCacheKey = ({ url, form, field }: { url: string, form: string, field: string }) => {
|
|
||||||
const raw = `${url}::${form}::${field}`;
|
|
||||||
const hash = crypto.createHash('sha1').update(raw).digest('hex');
|
|
||||||
return `${hash}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function removeLocaleFromPath(path: string) {
|
|
||||||
return '/' + path.split('/').slice(2).join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function castTextToTypeGiven(value: string, type: string) {
|
|
||||||
try {
|
|
||||||
switch (type) {
|
|
||||||
case 'decimal':
|
|
||||||
return parseFloat(value);
|
|
||||||
case 'number':
|
|
||||||
return Number(value);
|
|
||||||
case 'boolean':
|
|
||||||
return Boolean(value);
|
|
||||||
default:
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"title": "Welcome Ninja"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"title": "Hoşgeldiniz Ninja"
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ services:
|
||||||
dockerfile: ServicesApi/Dockerfile
|
dockerfile: ServicesApi/Dockerfile
|
||||||
target: builder
|
target: builder
|
||||||
ports:
|
ports:
|
||||||
- "8001:8001"
|
- "3000:3000"
|
||||||
env_file:
|
env_file:
|
||||||
- api_env.env
|
- api_env.env
|
||||||
environment:
|
environment:
|
||||||
|
|
|
||||||
129
proxy-md.md
129
proxy-md.md
|
|
@ -1,129 +0,0 @@
|
||||||
```docker-compose.yml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
nginx:
|
|
||||||
image: owasp/modsecurity:nginx
|
|
||||||
container_name: secure_nginx
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
- ./nginx/html:/usr/share/nginx/html:ro
|
|
||||||
- ./nginx/modsecurity/modsecurity.conf:/etc/modsecurity/modsecurity.conf:ro
|
|
||||||
- ./nginx/log:/var/log/nginx
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
fail2ban:
|
|
||||||
image: crazymax/fail2ban:latest
|
|
||||||
container_name: fail2ban
|
|
||||||
volumes:
|
|
||||||
- ./fail2ban/jail.local:/data/jail.local:ro
|
|
||||||
- ./fail2ban/filter.d:/data/filter.d:ro
|
|
||||||
- ./nginx/log:/var/log/nginx:ro
|
|
||||||
- fail2ban-data:/data
|
|
||||||
restart: always
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
- NET_RAW
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
fail2ban-data:
|
|
||||||
```
|
|
||||||
|
|
||||||
```conf
|
|
||||||
worker_processes 1;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
modsecurity on;
|
|
||||||
modsecurity_rules_file /etc/modsecurity/modsecurity.conf;
|
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
|
|
||||||
limit_req_zone $binary_remote_addr zone=limit1:10m rate=5r/s;
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
|
||||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
limit_req zone=limit1 burst=10 nodelay;
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
nginx/html/index.html
|
|
||||||
|
|
||||||
```
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>ModSecurity + Fail2Ban</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Güvenlik Duvarı Aktif!</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
nginx/modsecurity/modsecurity.conf
|
|
||||||
|
|
||||||
```
|
|
||||||
SecRuleEngine On
|
|
||||||
SecRequestBodyAccess On
|
|
||||||
SecResponseBodyAccess Off
|
|
||||||
|
|
||||||
SecRule REQUEST_HEADERS:User-Agent "curl" "id:10001,phase:1,deny,status:403,msg:'Curl client blocked'"
|
|
||||||
|
|
||||||
# Basit rate limit sayacı
|
|
||||||
SecAction "id:10010,phase:1,initcol:ip=%{REMOTE_ADDR},pass,nolog"
|
|
||||||
SecRule IP:REQCOUNT "@gt 20" "id:10011,phase:2,deny,status:429,msg:'Too many requests'"
|
|
||||||
SecAction "id:10012,phase:2,pass,nolog,setvar:ip.reqcount=+1"
|
|
||||||
```
|
|
||||||
|
|
||||||
fail2ban/jail.local
|
|
||||||
|
|
||||||
```
|
|
||||||
[nginx-req-limit]
|
|
||||||
enabled = true
|
|
||||||
filter = nginx-req-limit
|
|
||||||
action = iptables[name=HTTP, port=http, protocol=tcp]
|
|
||||||
logpath = /var/log/nginx/access.log
|
|
||||||
maxretry = 5
|
|
||||||
findtime = 60
|
|
||||||
bantime = 3600
|
|
||||||
```
|
|
||||||
|
|
||||||
fail2ban/filter.d/nginx-req-limit.conf
|
|
||||||
|
|
||||||
```
|
|
||||||
[Definition]
|
|
||||||
failregex = <HOST> -.* "(GET|POST).*HTTP.*" 429
|
|
||||||
ignoreregex =
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
mkdir -p nginx/html nginx/modsecurity fail2ban/filter.d
|
|
||||||
docker-compose up -d
|
|
||||||
Loading…
Reference in New Issue