updated frontend and auth backend service
This commit is contained in:
parent
0ce522d04a
commit
924b538559
|
|
@ -59,3 +59,4 @@ npm install ioredis
|
|||
npm install -D daisyui@latest
|
||||
npm install tailwindcss @tailwindcss/postcss daisyui@latest
|
||||
npm install lucide-react
|
||||
npm install next-crypto
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { IsObject, IsOptional, IsString, IsBoolean } from 'class-validator';
|
||||
import { IsOptional, IsString, IsBoolean } from 'class-validator';
|
||||
|
||||
export class userLoginValidator {
|
||||
@IsString()
|
||||
|
|
|
|||
|
|
@ -17,40 +17,149 @@ export class LoginService {
|
|||
const foundUser = await this.prisma.users.findFirstOrThrow({
|
||||
where: { email: dto.accessKey },
|
||||
});
|
||||
|
||||
if (foundUser.password_token) {
|
||||
throw new Error('Password need to be set first');
|
||||
}
|
||||
|
||||
const isPasswordValid = this.passHandlers.check_password(
|
||||
foundUser.uu_id,
|
||||
dto.password,
|
||||
foundUser.hash_password,
|
||||
);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
|
||||
const foundPerson = await this.prisma.people.findFirstOrThrow({
|
||||
where: { id: foundUser.id },
|
||||
});
|
||||
const redisData = AuthTokenSchema.parse({
|
||||
people: foundPerson,
|
||||
users: foundUser,
|
||||
credentials: {
|
||||
person_id: foundPerson.id,
|
||||
person_name: foundPerson.firstname,
|
||||
},
|
||||
selectionList: [],
|
||||
});
|
||||
const accessToken = await this.redis.setLoginToRedis(
|
||||
redisData,
|
||||
const alreadyExists = await this.redis.callExistingLoginToken(
|
||||
foundUser.uu_id,
|
||||
);
|
||||
return {
|
||||
accessToken,
|
||||
message: 'Login successful',
|
||||
};
|
||||
if (alreadyExists) {
|
||||
return {
|
||||
token: alreadyExists,
|
||||
message: 'User already logged in',
|
||||
};
|
||||
} else {
|
||||
let selectList: any[] = [];
|
||||
if (foundUser.user_type === 'occupant') {
|
||||
const livingSpaces = await this.prisma.build_living_space.findMany({
|
||||
where: { people: { id: foundPerson.id } },
|
||||
orderBy: {
|
||||
build_parts: {
|
||||
build: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
uu_id: true,
|
||||
occupant_types: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
occupant_code: true,
|
||||
occupant_type: true,
|
||||
function_retriever: true,
|
||||
},
|
||||
},
|
||||
build_parts: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
part_code: true,
|
||||
part_no: true,
|
||||
part_level: true,
|
||||
human_livable: true,
|
||||
api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
enum_class: true,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
build_name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
selectList = livingSpaces;
|
||||
} else if (foundUser.user_type === 'employee') {
|
||||
const employees = await this.prisma.employees.findMany({
|
||||
where: { people: { id: foundPerson.id } },
|
||||
orderBy: {
|
||||
staff: {
|
||||
duties: {
|
||||
departments: {
|
||||
companies: {
|
||||
formal_name: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
uu_id: true,
|
||||
staff: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
staff_code: true,
|
||||
function_retriever: true,
|
||||
duties: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
departments: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
department_code: true,
|
||||
department_name: true,
|
||||
companies: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
formal_name: true,
|
||||
public_name: true,
|
||||
addresses: {
|
||||
select: {
|
||||
comment_address: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
selectList = employees;
|
||||
}
|
||||
|
||||
const redisData = AuthTokenSchema.parse({
|
||||
people: foundPerson,
|
||||
users: foundUser,
|
||||
credentials: {
|
||||
person_uu_id: foundPerson.uu_id,
|
||||
person_name: foundPerson.firstname,
|
||||
person_full_name: `${foundPerson.firstname} ${foundPerson.middle_name || ''} | ${foundPerson.birthname || ''} | ${foundPerson.surname}`,
|
||||
},
|
||||
selectionList: {
|
||||
type: foundUser.user_type,
|
||||
list: selectList,
|
||||
},
|
||||
});
|
||||
|
||||
const accessToken = await this.redis.setLoginToRedis(
|
||||
redisData,
|
||||
foundUser.uu_id,
|
||||
);
|
||||
return {
|
||||
token: accessToken,
|
||||
message: 'Login successful',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ import { IsString } from 'class-validator';
|
|||
|
||||
export class userSelectValidator {
|
||||
@IsString()
|
||||
selected_uu_id: string;
|
||||
uuid: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,27 +28,53 @@ export class SelectService {
|
|||
);
|
||||
}
|
||||
const accessToken = accessObject.key.split(':')[1];
|
||||
console.log('accessToken', accessToken);
|
||||
|
||||
const existingSelectToken = await this.redis.callExistingSelectToken(
|
||||
accessObject.value.users.uu_id,
|
||||
dto.uuid,
|
||||
);
|
||||
if (existingSelectToken) {
|
||||
return {
|
||||
message: 'Select successful',
|
||||
token: existingSelectToken,
|
||||
};
|
||||
}
|
||||
const userType = accessObject.value.users.user_type;
|
||||
if (userType === 'employee') {
|
||||
const employee = await this.prisma.employees.findFirstOrThrow({
|
||||
where: { uu_id: dto.selected_uu_id },
|
||||
where: { uu_id: dto.uuid },
|
||||
omit: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
const staff = await this.prisma.staff.findFirstOrThrow({
|
||||
where: { id: employee.staff_id },
|
||||
omit: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
const duties = await this.prisma.duties.findFirstOrThrow({
|
||||
where: { id: staff.duties_id },
|
||||
omit: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
const department = await this.prisma.departments.findFirstOrThrow({
|
||||
where: { id: duties.department_id },
|
||||
omit: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
const duty = await this.prisma.duty.findFirstOrThrow({
|
||||
where: { id: duties.duties_id },
|
||||
omit: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
const company = await this.prisma.companies.findFirstOrThrow({
|
||||
where: { id: duties.company_id },
|
||||
omit: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
const employeeToken = EmployeeTokenSchema.parse({
|
||||
|
|
@ -59,9 +85,43 @@ export class SelectService {
|
|||
staff: staff,
|
||||
menu: null,
|
||||
pages: null,
|
||||
config: null,
|
||||
caches: null,
|
||||
selection: null,
|
||||
selection: await this.prisma.employees.findFirstOrThrow({
|
||||
where: { uu_id: dto.uuid },
|
||||
select: {
|
||||
uu_id: true,
|
||||
staff: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
staff_code: true,
|
||||
function_retriever: true,
|
||||
duties: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
departments: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
department_code: true,
|
||||
department_name: true,
|
||||
companies: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
formal_name: true,
|
||||
public_name: true,
|
||||
addresses: {
|
||||
select: {
|
||||
comment_address: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
functionsRetriever: staff.function_retriever,
|
||||
kind: UserType.employee,
|
||||
});
|
||||
|
|
@ -70,7 +130,7 @@ export class SelectService {
|
|||
accessToken,
|
||||
employeeToken,
|
||||
accessObject.value.users.uu_id,
|
||||
dto.selected_uu_id,
|
||||
dto.uuid,
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
@ -79,19 +139,54 @@ export class SelectService {
|
|||
};
|
||||
} else if (userType === 'occupant') {
|
||||
const livingSpace = await this.prisma.build_living_space.findFirstOrThrow(
|
||||
{ where: { uu_id: dto.selected_uu_id } },
|
||||
{
|
||||
where: { uu_id: dto.uuid },
|
||||
omit: {
|
||||
id: true,
|
||||
person_id: true,
|
||||
build_parts_id: true,
|
||||
occupant_type_id: true,
|
||||
ref_id: true,
|
||||
replication_id: true,
|
||||
cryp_uu_id: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
const occupantType = await this.prisma.occupant_types.findFirstOrThrow({
|
||||
where: { id: livingSpace.occupant_type_id },
|
||||
where: { uu_id: livingSpace.occupant_type_uu_id },
|
||||
omit: {
|
||||
id: true,
|
||||
cryp_uu_id: true,
|
||||
ref_id: true,
|
||||
replication_id: true,
|
||||
},
|
||||
});
|
||||
const part = await this.prisma.build_parts.findFirstOrThrow({
|
||||
where: { id: livingSpace.build_parts_id },
|
||||
where: { uu_id: livingSpace.build_parts_uu_id },
|
||||
omit: {
|
||||
id: true,
|
||||
cryp_uu_id: true,
|
||||
ref_id: true,
|
||||
replication_id: true,
|
||||
},
|
||||
});
|
||||
const build = await this.prisma.build.findFirstOrThrow({
|
||||
where: { id: part.build_id },
|
||||
where: { uu_id: part.build_uu_id },
|
||||
omit: {
|
||||
id: true,
|
||||
cryp_uu_id: true,
|
||||
ref_id: true,
|
||||
replication_id: true,
|
||||
},
|
||||
});
|
||||
const company = await this.prisma.companies.findFirstOrThrow({
|
||||
where: { uu_id: accessObject.value.users.related_company },
|
||||
omit: {
|
||||
id: true,
|
||||
cryp_uu_id: true,
|
||||
ref_id: true,
|
||||
replication_id: true,
|
||||
},
|
||||
});
|
||||
const occupantToken = OccupantTokenSchema.parse({
|
||||
livingSpace: livingSpace,
|
||||
|
|
@ -103,7 +198,42 @@ export class SelectService {
|
|||
pages: null,
|
||||
config: null,
|
||||
caches: null,
|
||||
selection: null,
|
||||
selection: await this.prisma.build_living_space.findFirstOrThrow({
|
||||
where: { uu_id: dto.uuid },
|
||||
select: {
|
||||
uu_id: true,
|
||||
occupant_types: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
occupant_code: true,
|
||||
occupant_type: true,
|
||||
function_retriever: true,
|
||||
},
|
||||
},
|
||||
build_parts: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
part_code: true,
|
||||
part_no: true,
|
||||
part_level: true,
|
||||
human_livable: true,
|
||||
api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
enum_class: true,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
build_name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
functionsRetriever: occupantType.function_retriever,
|
||||
kind: UserType.occupant,
|
||||
});
|
||||
|
|
@ -111,7 +241,7 @@ export class SelectService {
|
|||
accessToken,
|
||||
occupantToken,
|
||||
accessObject.value.users.uu_id,
|
||||
dto.selected_uu_id,
|
||||
dto.uuid,
|
||||
);
|
||||
return {
|
||||
message: 'Select successful',
|
||||
|
|
|
|||
|
|
@ -19,16 +19,7 @@ export class CacheService {
|
|||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(value);
|
||||
}
|
||||
|
||||
async get_with_keys(listKeys: (string | null)[]): Promise<any | null> {
|
||||
const joinKeys = this.createRegexPattern(listKeys);
|
||||
const value = await this.client.get(joinKeys);
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(value);
|
||||
return { key, value: JSON.parse(value) };
|
||||
}
|
||||
|
||||
async set_with_ttl(key: string, value: any, ttl: number) {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ import { PrismaService } from './prisma.service';
|
|||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
|
||||
await app.listen(process.env.PORT ?? 8001);
|
||||
console.log(`🚀 Uygulama çalışıyor: ${await app.getUrl()}`);
|
||||
extractAndPersistRoutes(app, app.get(PrismaService));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ export type UserType = (typeof UserType)[keyof typeof UserType];
|
|||
|
||||
// Credentials
|
||||
export const CredentialsSchema = z.object({
|
||||
person_id: z.number(),
|
||||
person_uu_id: z.string(),
|
||||
person_name: z.string(),
|
||||
full_name: z.string(),
|
||||
});
|
||||
export type Credentials = z.infer<typeof CredentialsSchema>;
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ export const AuthTokenSchema = z.object({
|
|||
active: z.boolean(),
|
||||
is_notification_send: z.boolean(),
|
||||
is_email_send: z.boolean(),
|
||||
id: z.number(),
|
||||
// id: z.number(),
|
||||
uu_id: z.string(),
|
||||
expiry_starts: z.date(),
|
||||
expiry_ends: z.date(),
|
||||
|
|
@ -77,7 +78,7 @@ export const AuthTokenSchema = z.object({
|
|||
active: z.boolean(),
|
||||
is_notification_send: z.boolean(),
|
||||
is_email_send: z.boolean(),
|
||||
id: z.number(),
|
||||
// id: z.number(),
|
||||
uu_id: z.string(),
|
||||
expiry_starts: z.date(),
|
||||
expiry_ends: z.date(),
|
||||
|
|
@ -86,14 +87,23 @@ export const AuthTokenSchema = z.object({
|
|||
default_language: z.string(),
|
||||
}),
|
||||
credentials: CredentialsSchema,
|
||||
selectionList: z.array(z.any()).optional().default([]),
|
||||
selectionList: z
|
||||
.object({
|
||||
type: z.string(),
|
||||
list: z.array(z.any()).optional().default([]),
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
type: '',
|
||||
list: [],
|
||||
}),
|
||||
});
|
||||
|
||||
export type AuthToken = z.infer<typeof AuthTokenSchema>;
|
||||
|
||||
export const EmployeeTokenSchema = z.object({
|
||||
company: z.object({
|
||||
id: z.number(),
|
||||
// id: z.number(),
|
||||
uu_id: z.string(),
|
||||
formal_name: z.string(),
|
||||
company_type: z.string(),
|
||||
|
|
@ -107,13 +117,13 @@ export const EmployeeTokenSchema = z.object({
|
|||
is_blacklist: z.boolean(),
|
||||
parent_id: z.number().nullable(),
|
||||
workplace_no: z.string().nullable(),
|
||||
official_address_id: z.number().nullable(),
|
||||
// official_address_id: z.number().nullable(),
|
||||
official_address_uu_id: z.string().nullable(),
|
||||
top_responsible_company_id: z.number().nullable(),
|
||||
top_responsible_company_uu_id: z.string().nullable(),
|
||||
ref_id: z.string().nullable(),
|
||||
replication_id: z.number(),
|
||||
cryp_uu_id: z.string().nullable(),
|
||||
// replication_id: z.number(),
|
||||
// cryp_uu_id: z.string().nullable(),
|
||||
created_credentials_token: z.string().nullable(),
|
||||
updated_credentials_token: z.string().nullable(),
|
||||
confirmed_credentials_token: z.string().nullable(),
|
||||
|
|
@ -129,17 +139,17 @@ export const EmployeeTokenSchema = z.object({
|
|||
ref_int: z.number().nullable(),
|
||||
}),
|
||||
department: z.object({
|
||||
id: z.number(),
|
||||
// id: z.number(),
|
||||
uu_id: z.string(),
|
||||
parent_department_id: z.number().nullable(),
|
||||
department_code: z.string(),
|
||||
department_name: z.string(),
|
||||
department_description: z.string(),
|
||||
company_id: z.number(),
|
||||
// company_id: z.number(),
|
||||
company_uu_id: z.string(),
|
||||
ref_id: z.string().nullable(),
|
||||
replication_id: z.number(),
|
||||
cryp_uu_id: z.string().nullable(),
|
||||
// ref_id: z.string().nullable(),
|
||||
// replication_id: z.number(),
|
||||
// cryp_uu_id: z.string().nullable(),
|
||||
created_credentials_token: z.string().nullable(),
|
||||
updated_credentials_token: z.string().nullable(),
|
||||
confirmed_credentials_token: z.string().nullable(),
|
||||
|
|
@ -155,14 +165,14 @@ export const EmployeeTokenSchema = z.object({
|
|||
ref_int: z.number().nullable(),
|
||||
}),
|
||||
duty: z.object({
|
||||
id: z.number(),
|
||||
// id: z.number(),
|
||||
uu_id: z.string(),
|
||||
duty_name: z.string(),
|
||||
duty_code: z.string(),
|
||||
duty_description: z.string(),
|
||||
ref_id: z.string().nullable(),
|
||||
replication_id: z.number(),
|
||||
cryp_uu_id: z.string().nullable(),
|
||||
// ref_id: z.string().nullable(),
|
||||
// replication_id: z.number(),
|
||||
// cryp_uu_id: z.string().nullable(),
|
||||
created_credentials_token: z.string().nullable(),
|
||||
updated_credentials_token: z.string().nullable(),
|
||||
confirmed_credentials_token: z.string().nullable(),
|
||||
|
|
@ -178,15 +188,15 @@ export const EmployeeTokenSchema = z.object({
|
|||
ref_int: z.number().nullable(),
|
||||
}),
|
||||
employee: z.object({
|
||||
id: z.number(),
|
||||
// id: z.number(),
|
||||
uu_id: z.string(),
|
||||
staff_id: z.number(),
|
||||
staff_uu_id: z.string(),
|
||||
people_id: z.number(),
|
||||
people_uu_id: z.string(),
|
||||
ref_id: z.string().nullable(),
|
||||
replication_id: z.number(),
|
||||
cryp_uu_id: z.string().nullable(),
|
||||
// ref_id: z.string().nullable(),
|
||||
// replication_id: z.number(),
|
||||
// cryp_uu_id: z.string().nullable(),
|
||||
created_credentials_token: z.string().nullable(),
|
||||
updated_credentials_token: z.string().nullable(),
|
||||
confirmed_credentials_token: z.string().nullable(),
|
||||
|
|
@ -202,17 +212,17 @@ export const EmployeeTokenSchema = z.object({
|
|||
ref_int: z.number().nullable(),
|
||||
}),
|
||||
staff: z.object({
|
||||
id: z.number(),
|
||||
// id: z.number(),
|
||||
uu_id: z.string(),
|
||||
staff_description: z.string(),
|
||||
staff_name: z.string(),
|
||||
staff_code: z.string(),
|
||||
duties_id: z.number(),
|
||||
// duties_id: z.number(),
|
||||
duties_uu_id: z.string(),
|
||||
function_retriever: z.string().nullable(),
|
||||
ref_id: z.string().nullable(),
|
||||
replication_id: z.number(),
|
||||
cryp_uu_id: z.string().nullable(),
|
||||
// ref_id: z.string().nullable(),
|
||||
// replication_id: z.number(),
|
||||
// cryp_uu_id: z.string().nullable(),
|
||||
created_credentials_token: z.string().nullable(),
|
||||
updated_credentials_token: z.string().nullable(),
|
||||
confirmed_credentials_token: z.string().nullable(),
|
||||
|
|
@ -230,8 +240,6 @@ export const EmployeeTokenSchema = z.object({
|
|||
|
||||
menu: z.array(z.object({})).nullable(),
|
||||
pages: z.array(z.string()).nullable(),
|
||||
// config: z.record(z.string(), z.unknown()).nullable(),
|
||||
// caches: z.record(z.string(), z.unknown()).nullable(),
|
||||
|
||||
selection: z.record(z.string(), z.unknown()).nullable(),
|
||||
functionsRetriever: z.string(),
|
||||
|
|
@ -247,8 +255,6 @@ export const OccupantTokenSchema = z.object({
|
|||
|
||||
menu: z.array(z.object({})).nullable(),
|
||||
pages: z.array(z.string()).nullable(),
|
||||
// config: z.record(z.string(), z.unknown()).nullable(),
|
||||
// caches: z.record(z.string(), z.unknown()).nullable(),
|
||||
|
||||
selection: z.record(z.string(), z.unknown()).nullable(),
|
||||
functionsRetriever: z.string(),
|
||||
|
|
|
|||
|
|
@ -71,6 +71,18 @@ export class RedisHandlers {
|
|||
return mergedRedisKey;
|
||||
}
|
||||
|
||||
public mergeLoginUser(userUUID: string) {
|
||||
const mergedRedisKey = `${this.AUTH_TOKEN}:*:*:${userUUID}:${userUUID}`;
|
||||
this.validateRedisKey(mergedRedisKey, this.AUTH_TOKEN);
|
||||
return mergedRedisKey;
|
||||
}
|
||||
|
||||
public mergeSelectUser(userUUID: string, livingUUID: string) {
|
||||
const mergedRedisKey = `${this.SELECT_TOKEN}:*:*:${userUUID}:${livingUUID}`;
|
||||
this.validateRedisKey(mergedRedisKey, this.SELECT_TOKEN);
|
||||
return mergedRedisKey;
|
||||
}
|
||||
|
||||
generateSelectToken(accessToken: string, userUUID: string) {
|
||||
return this.passwordService.createSelectToken(accessToken, userUUID);
|
||||
}
|
||||
|
|
@ -154,6 +166,53 @@ export class RedisHandlers {
|
|||
: null;
|
||||
}
|
||||
|
||||
async callExistingLoginToken(userUUID: string): Promise<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> {
|
||||
const mergedKey = this.mergeLoginKey(req);
|
||||
return this.cacheService.delete(mergedKey);
|
||||
|
|
|
|||
|
|
@ -16,5 +16,31 @@
|
|||
},
|
||||
"LocaleLayout": {
|
||||
"title": "Next.js i18n Application"
|
||||
},
|
||||
"Select": {
|
||||
"title": "Select Your Option",
|
||||
"description": "Please select one of the following options to continue",
|
||||
"employee": "Employee",
|
||||
"staff": "Staff",
|
||||
"uuid": "UUID",
|
||||
"department": "Department",
|
||||
"name": "Name",
|
||||
"code": "Code",
|
||||
"company": "Company",
|
||||
"occupant": "Occupant",
|
||||
"occupant_code": "Occupant Code",
|
||||
"building": "Building",
|
||||
"type": "Type",
|
||||
"part_details": "Part Details",
|
||||
"no": "No",
|
||||
"level": "Level",
|
||||
"status": "Status",
|
||||
"livable": "Livable",
|
||||
"not_livable": "Not Livable",
|
||||
"selection": "Selection",
|
||||
"id": "ID",
|
||||
"processing": "Processing...",
|
||||
"continue": "Continue",
|
||||
"select_option": "Select an option to continue"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,31 @@
|
|||
},
|
||||
"LocaleLayout": {
|
||||
"title": "Next.js i18n Uygulaması"
|
||||
},
|
||||
"Select": {
|
||||
"title": "Seçeneğinizi Seçin",
|
||||
"description": "Devam etmek için lütfen aşağıdaki seçeneklerden birini seçin",
|
||||
"employee": "Çalışan",
|
||||
"staff": "Personel",
|
||||
"uuid": "UUID",
|
||||
"department": "Departman",
|
||||
"name": "İsim",
|
||||
"code": "Kod",
|
||||
"company": "Şirket",
|
||||
"occupant": "Oturak",
|
||||
"occupant_code": "Oturak Kodu",
|
||||
"building": "Bina",
|
||||
"type": "Tip",
|
||||
"part_details": "Parça Detayları",
|
||||
"no": "No",
|
||||
"level": "Seviye",
|
||||
"status": "Durum",
|
||||
"livable": "Yaşanabilir",
|
||||
"not_livable": "Yaşanamaz",
|
||||
"selection": "Seçim",
|
||||
"id": "ID",
|
||||
"processing": "İşleniyor...",
|
||||
"continue": "Devam Et",
|
||||
"select_option": "Devam etmek için bir seçenek seçin"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"cookies-next": "^6.1.0",
|
||||
"ioredis": "^5.6.1",
|
||||
"lucide-react": "^0.533.0",
|
||||
"next": "15.4.4",
|
||||
"next-crypto": "^1.0.8",
|
||||
"next-intl": "^4.3.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
|
|
@ -1154,6 +1156,28 @@
|
|||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cookies-next": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-6.1.0.tgz",
|
||||
"integrity": "sha512-8MqWliHg6YRatqlup5HlKCqXM5cFtwq9BVowDpPniPfbTOmrfIEXUQOcRFVXQltV+hyvKDRGJPNtceICkiJ/IA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": ">=15.0.0",
|
||||
"react": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
|
|
@ -1681,6 +1705,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-crypto": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/next-crypto/-/next-crypto-1.0.8.tgz",
|
||||
"integrity": "sha512-6VcrH+xFuuCRGCdDMjFFibhJ97c4s+J/6SEV73RUYJhh38MDW4WXNZNTWIMZBq0B29LOIfAQ0XA37xGUZZCCjA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next-intl": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.4.tgz",
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"cookies-next": "^6.1.0",
|
||||
"ioredis": "^5.6.1",
|
||||
"lucide-react": "^0.533.0",
|
||||
"next": "15.4.4",
|
||||
"next-crypto": "^1.0.8",
|
||||
"next-intl": "^4.3.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
|
|
|
|||
|
|
@ -3,109 +3,60 @@ import Link from 'next/link';
|
|||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { z } from 'zod';
|
||||
import { Eye, EyeOff, Lock, Mail, User } from "lucide-react";
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email('Invalid email'),
|
||||
password: z.string().min(6, 'Password too short')
|
||||
});
|
||||
|
||||
type LoginFormData = z.infer<typeof loginSchema>;
|
||||
|
||||
const getRandomColor = () => {
|
||||
const colors = ['bg-indigo-500', 'bg-purple-500', 'bg-pink-500', 'bg-blue-500', 'bg-teal-500'];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
};
|
||||
import { Eye, EyeOff, Lock, Mail } from "lucide-react";
|
||||
import { apiPostFetcher } from '@/lib/fetcher';
|
||||
import { useRouter } from '@/i18n/routing';
|
||||
|
||||
export default function LoginPage() {
|
||||
const t = useTranslations('Login');
|
||||
const [formData, setFormData] = useState<LoginFormData>({
|
||||
email: '',
|
||||
password: ''
|
||||
const loginSchema = z.object({
|
||||
accessKey: z.string().email(t('emailWrong')),
|
||||
password: z.string().min(6, t('passwordWrong')),
|
||||
rememberMe: z.boolean().default(false),
|
||||
});
|
||||
const [errors, setErrors] = useState<Partial<LoginFormData>>({});
|
||||
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 [isAccordionOpen, setIsAccordionOpen] = useState(false);
|
||||
const recentUser = {
|
||||
name: 'Mika Lee',
|
||||
initial: 'M',
|
||||
color: getRandomColor()
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const router = useRouter();
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
loginSchema.parse(formData);
|
||||
setErrors({});
|
||||
console.log('Form submitted:', formData);
|
||||
// Here you would typically call your authentication API
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) {
|
||||
const fieldErrors: Partial<LoginFormData> = {};
|
||||
Object.entries(err.flatten().fieldErrors).forEach(([key, value]) => {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
const fieldKey = key as keyof LoginFormData;
|
||||
fieldErrors[fieldKey] = value[0];
|
||||
}
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
const form = e.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
const loginData: LoginInterface = {
|
||||
accessKey: formData.get('email') as string,
|
||||
password: formData.get('password') as string,
|
||||
rememberMe: formData.get('rememberMe') === 'on'
|
||||
};
|
||||
const result = loginSchema.safeParse(loginData);
|
||||
if (!result.success) {
|
||||
const fieldErrors: LoginFormErrors = {};
|
||||
if (result.error.issues.some(issue => issue.path.includes('email'))) {
|
||||
fieldErrors.accessKey = true;
|
||||
}
|
||||
if (result.error.issues.some(issue => issue.path.includes('password'))) {
|
||||
fieldErrors.password = true;
|
||||
}
|
||||
setErrors(fieldErrors);
|
||||
} else {
|
||||
setErrors({})
|
||||
console.log('Form submitted successfully:', loginData);
|
||||
apiPostFetcher({ url: '/api/auth/login', body: loginData, isNoCache: true }).then((res) => {
|
||||
if (res.success) {
|
||||
console.log('Login successful, redirecting to select page');
|
||||
router.push('/select');
|
||||
}
|
||||
}).catch((error) => { console.error('Login failed:', error) });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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">
|
||||
{/* Recent logins accordion */}
|
||||
{/* <div className="card bg-white/90 backdrop-blur-sm shadow-xl border border-indigo-100 rounded-2xl w-full mb-4">
|
||||
<div className="card-body p-4 sm:p-6">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
onClick={() => setIsAccordionOpen(!isAccordionOpen)}
|
||||
>
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="avatar placeholder">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 text-white flex items-center justify-center">
|
||||
<User className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg sm:text-xl font-bold text-gray-800">{t('recentLogins')}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-5 h-5 text-gray-500 transition-transform duration-300 ${isAccordionOpen ? 'rotate-180' : ''}`}
|
||||
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 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{isAccordionOpen && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<p className="text-xs sm:text-sm text-gray-500 mb-4">{t('clickPictureOrAdd')}</p>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 sm:gap-3 w-full">
|
||||
<div className="flex flex-col items-center cursor-pointer group">
|
||||
<div className="w-full aspect-square mb-1 sm:mb-2 bg-gradient-to-br from-gray-100 to-gray-200 rounded-2xl flex items-center justify-center transition-all duration-300 group-hover:scale-105 group-hover:shadow-lg border-2 border-dashed border-indigo-200 group-hover:border-indigo-400">
|
||||
<span className="text-xl sm:text-2xl text-indigo-400 group-hover:text-indigo-600 transition-colors">+</span>
|
||||
</div>
|
||||
<span className="text-xs sm:text-sm font-medium text-gray-600 group-hover:text-indigo-600 transition-colors">
|
||||
{t('addAccount')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<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">
|
||||
|
|
@ -122,27 +73,25 @@ export default function LoginPage() {
|
|||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="email"
|
||||
type="text"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
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.email ? 'border-red-500 focus:border-red-500 focus:ring-red-200' : ''}`}
|
||||
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',
|
||||
}}
|
||||
// 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.email && (
|
||||
{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>
|
||||
{errors.email}
|
||||
{t('emailWrong')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -156,14 +105,12 @@ export default function LoginPage() {
|
|||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
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',
|
||||
}}
|
||||
// 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
|
||||
|
|
@ -181,7 +128,7 @@ export default function LoginPage() {
|
|||
<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>
|
||||
{errors.password}
|
||||
{t('passwordWrong')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -189,7 +136,7 @@ export default function LoginPage() {
|
|||
|
||||
<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 type="checkbox" className="checkbox checkbox-primary checkbox-sm [--chkbg:theme(colors.indigo.500)] [--chkfg:theme(colors.white)] border-indigo-300" />
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
'use server';
|
||||
import LocaleSwitcherServer from '@/components/LocaleSwitcherServer';
|
||||
import LoginPage from './LoginPage';
|
||||
import FromFigma from './fromFigma';
|
||||
import { Locale } from 'next-intl';
|
||||
import { checkAccessOnLoginPage } from '@/app/api/guards';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
export default async function PageLogin({ params }: Props) {
|
||||
export default async function PageLogin({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
await checkAccessOnLoginPage(locale as Locale);
|
||||
return (
|
||||
<div>
|
||||
<div className='absolute top-2 right-2'>
|
||||
<LocaleSwitcherServer locale={locale} pathname="/login" />
|
||||
</div>
|
||||
<LoginPage />
|
||||
{/* <FromFigma /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { apiGetFetcher } from '@/lib/fetcher';
|
||||
|
||||
export default function PageSelect() {
|
||||
const t = useTranslations('Select');
|
||||
const router = useRouter();
|
||||
const [selectionList, setSelectionList] = useState<{ type: string, list: any[] } | null>(null);
|
||||
const [selectedOption, setSelectedOption] = useState<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,3 +1,11 @@
|
|||
export default function SelectPage() {
|
||||
return <div></div>;
|
||||
'use server';
|
||||
import { Locale } from 'next-intl';
|
||||
import { checkAccess, checkSelectionOnSelectPage } from '@/app/api/guards';
|
||||
import SelectPageClient from './SelectPage';
|
||||
|
||||
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 />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,18 @@ function removeSubStringFromPath(headersList: Headers) {
|
|||
return removeLocaleFromPath(currentRoute);
|
||||
}
|
||||
|
||||
function getLocaleFromPath(path: string) {
|
||||
const locale = path.split('/')[0];
|
||||
return locale;
|
||||
}
|
||||
|
||||
export default async function ProtectedLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode,
|
||||
}) {
|
||||
// Server component approach to get URL
|
||||
const headersList = await headers();
|
||||
// const locale = getLocaleFromPath(removeSubStringFromPath(headersList));
|
||||
const removedLocaleRoute = removeSubStringFromPath(headersList);
|
||||
console.log('Removed locale route:', removedLocaleRoute);
|
||||
return <>{children}</>;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export default function OfficePage() {
|
||||
return <div></div>;
|
||||
return <div>Office Page</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export default function VenuePage() {
|
||||
return <div></div>;
|
||||
return <div>Venue Page</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ type SearchFormData = z.infer<typeof searchFormSchema>;
|
|||
|
||||
export default function TrialPage() {
|
||||
const pathname = usePathname();
|
||||
const cleanPathname = removeLocaleFromPath(pathname);
|
||||
const cleanPathname = removeLocaleFromPath(pathname || '');
|
||||
|
||||
const cacheKeyCreateForm = buildCacheKey({ url: cleanPathname, form: 'trialCreateForm', field: 'trialCreateField' });
|
||||
const cacheKeySelectForm = buildCacheKey({ url: cleanPathname, form: 'trialSelectForm', field: 'trialSelectField' });
|
||||
|
|
@ -67,7 +67,7 @@ export default function TrialPage() {
|
|||
} catch (error) {
|
||||
console.error('Error saving form data:', error);
|
||||
setCreateFormErrors({
|
||||
error: "Error saving form data"
|
||||
error: "Error saving form data"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { isAccessTokenValid } from "@/fetchers/token/access";
|
||||
|
||||
export async function GET() {
|
||||
const isValid = await isAccessTokenValid();
|
||||
return !isValid
|
||||
? NextResponse.json({ ok: false }, { status: 401 })
|
||||
: NextResponse.json({ ok: true }, { status: 200 });
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { isSelectTokenValid } from "@/fetchers/token/select";
|
||||
|
||||
export async function GET() {
|
||||
const isSlcTokenValid = await isSelectTokenValid();
|
||||
return !isSlcTokenValid
|
||||
? NextResponse.json({ ok: false }, { status: 401 })
|
||||
: NextResponse.json({ ok: true }, { status: 200 });
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { doLogin } from "@/fetchers/auth/login/fetch";
|
||||
import { setCookieAccessToken } from "@/fetchers/token/cookies";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { accessKey, password, rememberMe } = await req.json();
|
||||
console.log("Login attempt for:", accessKey);
|
||||
const response = await doLogin({ accessKey, password, rememberMe });
|
||||
if (response.status !== 200) {
|
||||
console.log("Login failed with status:", response.status);
|
||||
return NextResponse.json({ status: 401 });
|
||||
}
|
||||
const data = response.data as any;
|
||||
const token = data.token;
|
||||
console.log("Token received:", token ? "[PRESENT]" : "[MISSING]");
|
||||
if (!token) {
|
||||
console.error("No token received from login response");
|
||||
return NextResponse.json({ status: 500, message: "No token received" });
|
||||
}
|
||||
await setCookieAccessToken(token);
|
||||
console.log("Cookie set via setCookieAccessToken");
|
||||
return NextResponse.json({ status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error in login route:", error);
|
||||
return NextResponse.json({ status: 500, message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { doSelect } from "@/fetchers/auth/login/fetch";
|
||||
import { setCookieSelectToken } from "@/fetchers/token/cookies";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { uuid } = await req.json();
|
||||
console.log("Select attempt for UUID:", uuid);
|
||||
|
||||
const response = await doSelect({ uuid });
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Select failed with status:", response.status);
|
||||
return NextResponse.json({ status: 401, message: "Select failed" });
|
||||
}
|
||||
|
||||
const data = response.data as any;
|
||||
const token = data.token;
|
||||
console.log("Select token received:", token ? "[PRESENT]" : "[MISSING]");
|
||||
|
||||
if (!token) {
|
||||
console.error("No token received from select response");
|
||||
return NextResponse.json({ status: 500, message: "No token received" });
|
||||
}
|
||||
|
||||
// Set the cookie using the server-side utility function
|
||||
await setCookieSelectToken(token);
|
||||
console.log("Select cookie set via setCookieSelectToken");
|
||||
|
||||
// Return the response
|
||||
return NextResponse.json({ status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error in select route:", error);
|
||||
return NextResponse.json({ status: 500, message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getSelectionList } from "@/fetchers/auth/selection/list/fetch";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
return await getSelectionList();
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { deleteKey } from "@/libss/redisService";
|
||||
import { deleteKey } from "@/fetchers/redis/redisService";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { key } = await req.json();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { exists } from "@/libss/redisService";
|
||||
import { exists } from "@/fetchers/redis/redisService";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { key } = await req.json();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getJSON } from "@/libss/redisService";
|
||||
import { getJSON } from "@/fetchers/redis/redisService";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { key } = await req.json();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { updateField } from "@/libss/redisService";
|
||||
import { updateField } from "@/fetchers/redis/redisService";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
setJSON,
|
||||
getJSON,
|
||||
updateJSON,
|
||||
deleteKey,
|
||||
exists,
|
||||
} from "@/libss/redisService";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { action, key, value, ttlSeconds } = await req.json();
|
||||
|
||||
switch (action) {
|
||||
case "set":
|
||||
await setJSON({ key, value, ttlSeconds });
|
||||
return NextResponse.json({ status: "ok", message: `Set ${key}` });
|
||||
|
||||
case "get": {
|
||||
const result = await getJSON({ key });
|
||||
return result
|
||||
? NextResponse.json({ status: "ok", data: result })
|
||||
: NextResponse.json({ status: "not_found" }, { status: 404 });
|
||||
}
|
||||
|
||||
case "update":
|
||||
await updateJSON({ key, value });
|
||||
return NextResponse.json({ status: "ok", message: `Updated ${key}` });
|
||||
|
||||
case "delete":
|
||||
await deleteKey({ key });
|
||||
return NextResponse.json({ status: "ok", message: `Deleted ${key}` });
|
||||
|
||||
case "exists":
|
||||
const doesExist = await exists({ key });
|
||||
return NextResponse.json({ status: "ok", exists: doesExist });
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("[redis-api] error:", e.message);
|
||||
return NextResponse.json({ error: e.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { setJSON } from "@/libss/redisService";
|
||||
import { setJSON } from "@/fetchers/redis/redisService";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { key, value, ttlSeconds } = await req.json();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { updateJSON } from "@/libss/redisService";
|
||||
import { updateJSON } from "@/fetchers/redis/redisService";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
'use server';
|
||||
import { redirect } from '@/i18n/navigation';
|
||||
import { Locale } from 'next-intl';
|
||||
import { isAccessTokenValid } from '@/fetchers/token/access';
|
||||
import { isSelectTokenValid } from '@/fetchers/token/select';
|
||||
|
||||
async function checkAccessOnLoginPage(locale: Locale) {
|
||||
const access = await isAccessTokenValid();
|
||||
if (access) {
|
||||
return redirect({ href: '/select', locale: locale });
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAccess(locale: Locale) {
|
||||
const access = await isAccessTokenValid();
|
||||
if (!access) {
|
||||
return redirect({ href: '/login', locale: locale });
|
||||
}
|
||||
}
|
||||
|
||||
async function checkSelectionOnSelectPage(locale: Locale) {
|
||||
const select = await isSelectTokenValid();
|
||||
if (select) {
|
||||
return redirect({ href: '/venue', locale: locale });
|
||||
}
|
||||
}
|
||||
|
||||
async function checkSelection(locale: Locale) {
|
||||
const select = await isSelectTokenValid();
|
||||
if (!select) {
|
||||
return redirect({ href: '/select', locale: locale });
|
||||
} 1
|
||||
}
|
||||
|
||||
export { checkAccess, checkSelection, checkAccessOnLoginPage, checkSelectionOnSelectPage };
|
||||
|
|
@ -6,6 +6,8 @@ import LocaleSwitcherClient from '@/components/LocaleSwitcherClient';
|
|||
|
||||
export default function HomePage() {
|
||||
const t = useTranslations('Index');
|
||||
const n = useTranslations('Index.navigation');
|
||||
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
|
|
@ -18,11 +20,13 @@ export default function HomePage() {
|
|||
<main>
|
||||
<h1>{t('title')}</h1>
|
||||
<p>{t('description')}</p>
|
||||
<p>{t('navigation.title')} : {params.locale}</p>
|
||||
<p>{n('title')} : {params?.locale || 'tr'}</p>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<LocaleSwitcherClient />
|
||||
<button onClick={() => handleNavigation('/about')}>{t('navigation.about')}</button>
|
||||
<button onClick={() => handleNavigation('/home')}>{t('navigation.home')}</button>
|
||||
<button onClick={() => handleNavigation('/about')}>{n('about')}</button>
|
||||
<button onClick={() => handleNavigation('/home')}>{n('home')}</button>
|
||||
<button onClick={() => handleNavigation('/login')}>{n('login')}</button>
|
||||
<button onClick={() => handleNavigation('/select')}>{n('select')}</button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
'use server';
|
||||
import { fetchData } from "@/fetchers/fecther";
|
||||
import { urlSelectEndpoint, urlLoginEndpoint } from "@/fetchers/urls";
|
||||
import { LoginViaAccessKeys } from "@/fetchers/types/login/validations";
|
||||
import { setCookieAccessToken, setCookieSelectToken } from "@/fetchers/token/cookies";
|
||||
|
||||
|
||||
async function doLogin(payload: LoginViaAccessKeys) {
|
||||
const response = await fetchData(
|
||||
urlLoginEndpoint,
|
||||
payload,
|
||||
"POST",
|
||||
false,
|
||||
5000
|
||||
);
|
||||
if (response.status !== 200) {
|
||||
console.log('doLogin response', response);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async function doSelect(payload: { uuid: string }) {
|
||||
const response = await fetchData(
|
||||
urlSelectEndpoint,
|
||||
payload,
|
||||
"POST",
|
||||
false,
|
||||
5000
|
||||
);
|
||||
if (response.status == 200) {
|
||||
console.log('doSelect response', response);
|
||||
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export { doLogin, doSelect };
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
'use server';
|
||||
import { getAccessObjectField } from "@/fetchers/token/access";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
async function getSelectionList() {
|
||||
const selectionList = await getAccessObjectField("selectionList");
|
||||
if (!selectionList) return NextResponse.json({ success: false, message: "Selection list not found" });
|
||||
return NextResponse.json({ success: true, data: selectionList, message: "Selection list fetched successfully" });
|
||||
}
|
||||
|
||||
|
||||
export { getSelectionList };
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies";
|
||||
import NextCrypto from "next-crypto";
|
||||
|
||||
const tokenSecretEnv = process.env.TOKENSECRET_90;
|
||||
const tokenSecret = tokenSecretEnv || "e781d1b0-9418-40b3-9940-385abf81a0b7";
|
||||
const REDIS_TIMEOUT = 5000;
|
||||
const nextCrypto = new NextCrypto(tokenSecret);
|
||||
|
||||
// Cookie options for cookies-next
|
||||
const cookieOptions = {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: "lax" as const,
|
||||
path: "/",
|
||||
maxAge: 30 * 60, // 30 minutes
|
||||
// priority: "high",
|
||||
};
|
||||
|
||||
const DEFAULT_TIMEOUT: number = 10000;
|
||||
const defaultHeaders: Record<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,
|
||||
};
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
"use server";
|
||||
import { defaultHeaders, DEFAULT_TIMEOUT } from "./base";
|
||||
import { FetchOptions, HttpMethod, ApiResponse } from "./types";
|
||||
import { getPlainAccessToken } from "./token/access";
|
||||
import { getPlainSelectToken } from "./token/select";
|
||||
|
||||
const DEFAULT_RESPONSE = {
|
||||
error: "Hata tipi belirtilmedi",
|
||||
status: 500,
|
||||
data: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a promise that rejects after a specified timeout
|
||||
* @param ms Timeout in milliseconds
|
||||
* @param controller AbortController to abort the fetch request
|
||||
* @returns A promise that rejects after the timeout
|
||||
*/
|
||||
const createTimeoutPromise = (
|
||||
ms: number,
|
||||
controller: AbortController
|
||||
): Promise<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,4 +1,5 @@
|
|||
import redis from "./redis";
|
||||
import { redisScanAccess, redisScanSelect } from "../types/base";
|
||||
|
||||
interface SScanParams {
|
||||
rKey: string;
|
||||
|
|
@ -90,13 +91,34 @@ export async function deleteKey(params: GetParams): RDelete {
|
|||
}
|
||||
|
||||
export async function scanByRKeySingle(params: SScanParams): RScan {
|
||||
const pattern = `X:${params.rKey}:${params.rKey}:*:*`;
|
||||
const [_, results] = await redis.scan("0", "MATCH", pattern, "COUNT", 1);
|
||||
return results.length > 0 ? results[0] : null;
|
||||
const pattern = redisScanAccess(params.rKey);
|
||||
const keys: string[] = [];
|
||||
let cursor = "0";
|
||||
do {
|
||||
const [nextCursor, matchedKeys] = await redis.scan(
|
||||
cursor,
|
||||
"MATCH",
|
||||
pattern
|
||||
);
|
||||
cursor = nextCursor;
|
||||
keys.push(...matchedKeys);
|
||||
} while (cursor !== "0");
|
||||
return keys.length > 0 ? keys[0] : null;
|
||||
}
|
||||
|
||||
export async function scanByRKeyDouble(params: DScanParams): RScan {
|
||||
const pattern = `X:${params.rKey}:${params.sKey}:*:*`;
|
||||
const [_, results] = await redis.scan("0", "MATCH", pattern, "COUNT", 1);
|
||||
return results.length > 0 ? results[0] : null;
|
||||
const pattern = redisScanSelect(params.rKey, params.sKey);
|
||||
console.log("pattern", pattern);
|
||||
const keys: string[] = [];
|
||||
let cursor = "0";
|
||||
do {
|
||||
const [nextCursor, matchedKeys] = await redis.scan(
|
||||
cursor,
|
||||
"MATCH",
|
||||
pattern
|
||||
);
|
||||
cursor = nextCursor;
|
||||
keys.push(...matchedKeys);
|
||||
} while (cursor !== "0");
|
||||
return keys.length > 0 ? keys[0] : null;
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
'use server';
|
||||
import { AuthError } from "@/fetchers/types/base";
|
||||
import { scanByRKeySingle, getJSON } from "@/fetchers/redis/redisService";
|
||||
import { nextCrypto } from "@/fetchers/base";
|
||||
import { getCookieAccessToken, removeCookieTokens, setCookieAccessToken } from "./cookies";
|
||||
|
||||
async function getPlainAccessToken() {
|
||||
try {
|
||||
let encryptedAccessToken = "";
|
||||
const accessCookie = await getCookieAccessToken();
|
||||
if (accessCookie?.value) {
|
||||
encryptedAccessToken = accessCookie.value;
|
||||
}
|
||||
if (!encryptedAccessToken) {
|
||||
throw new AuthError("No access token found");
|
||||
}
|
||||
try {
|
||||
const decryptedAccessToken = await nextCrypto.decrypt(encryptedAccessToken) || "";
|
||||
if (!decryptedAccessToken) {
|
||||
throw new AuthError("Access token is invalid");
|
||||
}
|
||||
return decryptedAccessToken;
|
||||
} catch (decryptError) {
|
||||
throw new AuthError("Failed to decrypt access token");
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
throw new AuthError("No access token found");
|
||||
}
|
||||
}
|
||||
|
||||
async function getAccessToken() {
|
||||
try {
|
||||
const plainAccessToken = await getPlainAccessToken();
|
||||
if (!plainAccessToken) {
|
||||
throw new AuthError("No access token found");
|
||||
}
|
||||
try {
|
||||
const scanToken = await scanByRKeySingle({ rKey: plainAccessToken });
|
||||
if (!scanToken) throw new AuthError("Access token is invalid");
|
||||
return scanToken;
|
||||
} catch (scanError) {
|
||||
throw new AuthError("Failed to validate access token");
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
throw new AuthError("No access token found");
|
||||
}
|
||||
}
|
||||
|
||||
async function getAccessObject() {
|
||||
try {
|
||||
const accessToken = await getAccessToken();
|
||||
if (!accessToken) {
|
||||
throw new AuthError("No access token found");
|
||||
}
|
||||
const accessObject = await getJSON({ key: accessToken });
|
||||
return accessObject;
|
||||
}
|
||||
catch (error) {
|
||||
throw new AuthError("No access token found");
|
||||
}
|
||||
}
|
||||
|
||||
async function getAccessObjectField(field: string): Promise<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 };
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
'use server';
|
||||
import { nextCrypto, cookieOptions } from "@/fetchers/base";
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const ACCESS_TOKEN_COOKIE = 'acs';
|
||||
const SELECT_TOKEN_COOKIE = 'slc';
|
||||
|
||||
export async function getCookieAccessToken() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const encryptedToken = cookieStore.get(ACCESS_TOKEN_COOKIE);
|
||||
if (!encryptedToken) {
|
||||
return undefined;
|
||||
}
|
||||
return { name: ACCESS_TOKEN_COOKIE, value: encryptedToken.value };
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setCookieAccessToken(token: string) {
|
||||
try {
|
||||
const encryptedToken = await nextCrypto.encrypt(token);
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(ACCESS_TOKEN_COOKIE, encryptedToken, {
|
||||
httpOnly: cookieOptions.httpOnly,
|
||||
secure: cookieOptions.secure,
|
||||
sameSite: cookieOptions.sameSite,
|
||||
path: cookieOptions.path,
|
||||
maxAge: cookieOptions.maxAge
|
||||
});
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCookieSelectToken() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const encryptedToken = cookieStore.get(SELECT_TOKEN_COOKIE);
|
||||
if (!encryptedToken) {
|
||||
return undefined;
|
||||
}
|
||||
return { name: SELECT_TOKEN_COOKIE, value: encryptedToken.value };
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setCookieSelectToken(token: string) {
|
||||
try {
|
||||
const encryptedToken = await nextCrypto.encrypt(token);
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(SELECT_TOKEN_COOKIE, encryptedToken, {
|
||||
httpOnly: cookieOptions.httpOnly,
|
||||
secure: cookieOptions.secure,
|
||||
sameSite: cookieOptions.sameSite,
|
||||
path: cookieOptions.path,
|
||||
maxAge: cookieOptions.maxAge
|
||||
});
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeAccessToken() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete(ACCESS_TOKEN_COOKIE);
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeSelectToken() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete(SELECT_TOKEN_COOKIE);
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCookieTokens() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete(ACCESS_TOKEN_COOKIE);
|
||||
cookieStore.delete(SELECT_TOKEN_COOKIE);
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
'use server';
|
||||
import { AuthError } from "@/fetchers/types/base";
|
||||
import { scanByRKeyDouble } from "@/fetchers/redis/redisService";
|
||||
import { getPlainAccessToken } from "./access";
|
||||
import { nextCrypto } from "@/fetchers/base";
|
||||
import { getCookieSelectToken, removeCookieTokens, setCookieSelectToken } from "./cookies";
|
||||
|
||||
async function getPlainSelectToken() {
|
||||
console.log('getPlainSelectToken is triggered...');
|
||||
|
||||
try {
|
||||
let encryptedSelectToken = "";
|
||||
|
||||
// Try to get token from cookies first
|
||||
const selectCookie = await getCookieSelectToken();
|
||||
if (selectCookie?.value) {
|
||||
encryptedSelectToken = selectCookie.value;
|
||||
console.log('Select token from cookie:', 'found');
|
||||
}
|
||||
|
||||
console.log('getPlainSelectToken encryptedSelectToken', encryptedSelectToken ? `${encryptedSelectToken.substring(0, 10)}...` : 'empty');
|
||||
|
||||
if (!encryptedSelectToken) {
|
||||
console.log('No select token found');
|
||||
throw new AuthError("No select token found");
|
||||
}
|
||||
|
||||
const decryptedSelectToken = await nextCrypto.decrypt(encryptedSelectToken) || "";
|
||||
console.log('getPlainSelectToken decryptedSelectToken', decryptedSelectToken ? `${decryptedSelectToken.substring(0, 10)}...` : 'empty');
|
||||
|
||||
if (!decryptedSelectToken) {
|
||||
console.log('Decrypted select token is invalid or empty');
|
||||
throw new AuthError("Select token is invalid");
|
||||
}
|
||||
|
||||
return decryptedSelectToken;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error in getPlainSelectToken:', error);
|
||||
throw new AuthError("No select token found on cookies");
|
||||
}
|
||||
}
|
||||
|
||||
async function getSelectToken() {
|
||||
try {
|
||||
const plainAccessToken = await getPlainAccessToken();
|
||||
const plainSelectToken = await getPlainSelectToken();
|
||||
console.log('plainAccessToken', plainAccessToken);
|
||||
console.log('plainSelectToken', plainSelectToken);
|
||||
const scanToken = await scanByRKeyDouble({ rKey: plainAccessToken, sKey: plainSelectToken });
|
||||
if (!scanToken) throw new AuthError("Select token is invalid");
|
||||
return scanToken;
|
||||
}
|
||||
catch (error) { throw new AuthError("No select token found in headers") }
|
||||
}
|
||||
|
||||
async function setSelectToken(token: string) {
|
||||
console.log('setSelectToken is triggered...');
|
||||
await setCookieSelectToken(token);
|
||||
}
|
||||
|
||||
async function isSelectTokenValid() {
|
||||
try {
|
||||
const selectToken = await getSelectToken();
|
||||
console.log('isSelectTokenValid selectToken', selectToken);
|
||||
if (selectToken) return true;
|
||||
} catch (error) {
|
||||
console.log('isSelectTokenValid error', error);
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSelectToken() {
|
||||
console.log('removeSelectToken is triggered');
|
||||
await removeCookieTokens();
|
||||
}
|
||||
|
||||
export { getSelectToken, setSelectToken, removeSelectToken, getPlainSelectToken, isSelectTokenValid };
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
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 };
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
export class AuthError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "AuthError";
|
||||
}
|
||||
}
|
||||
|
||||
const accessExt = "AUTH_TOKEN";
|
||||
const selectExt = "SELECT_TOKEN";
|
||||
function redisScanAccess(cookieToken: string) {
|
||||
return `${accessExt}:${cookieToken}:${cookieToken}:*:*`;
|
||||
}
|
||||
function redisScanSelect(cookieToken: string, selectToken: string) {
|
||||
return `${selectExt}:${cookieToken}:${selectToken}:*:*`;
|
||||
}
|
||||
|
||||
export { redisScanAccess, redisScanSelect };
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
interface LoginViaAccessKeys {
|
||||
accessKey: string;
|
||||
password: string;
|
||||
rememberMe: boolean;
|
||||
}
|
||||
|
||||
interface LoginSelect {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export type { LoginViaAccessKeys, LoginSelect };
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
const baseUrl = process.env.API_URL || "http://localhost:8001";
|
||||
const selfApi = process.env.SELF_API_URL || "http://localhost:3000";
|
||||
const urlLoginEndpoint = `${baseUrl}/auth/login`;
|
||||
const urlSelectEndpoint = `${baseUrl}/auth/select`;
|
||||
|
||||
export { urlLoginEndpoint, urlSelectEndpoint, selfApi };
|
||||
|
|
@ -12,7 +12,9 @@
|
|||
"home": "Home",
|
||||
"about": "About",
|
||||
"contact": "Contact",
|
||||
"current": "Current Language"
|
||||
"current": "Current Language",
|
||||
"login": "Login",
|
||||
"select": "Select"
|
||||
}
|
||||
},
|
||||
"LocaleLayout": {
|
||||
|
|
@ -41,6 +43,52 @@
|
|||
"signUp": "Sign up",
|
||||
"copyright": "© 2023 Your Company. All rights reserved.",
|
||||
"welcomeBack": "Welcome Back",
|
||||
"continueJourney": "Sign in to continue your journey with us"
|
||||
"continueJourney": "Sign in to continue your journey with us",
|
||||
"emailWrong": "Please check email format",
|
||||
"passwordWrong": "Please check password format"
|
||||
},
|
||||
"Select": {
|
||||
"title": "Select Your Option",
|
||||
"description": "Please select one of the following options to continue",
|
||||
"employee": "Employee",
|
||||
"occupant_type": "Occupant Type",
|
||||
"staff": "Staff",
|
||||
"uuid": "UUID",
|
||||
"department": "Department",
|
||||
"name": "Name",
|
||||
"code": "Code",
|
||||
"company": "Company",
|
||||
"occupant": "Occupant",
|
||||
"occupant_code": "Occupant Code",
|
||||
"building": "Building",
|
||||
"type": "Type",
|
||||
"part_details": "Part Details",
|
||||
"no": "No",
|
||||
"level": "Level",
|
||||
"status": "Status",
|
||||
"livable": "Livable",
|
||||
"not_livable": "Not Livable",
|
||||
"selection": "Selection",
|
||||
"id": "ID",
|
||||
"processing": "Processing...",
|
||||
"continue": "Continue",
|
||||
"select_option": "Select an option to continue",
|
||||
"recentLogins": "Recent logins",
|
||||
"clickPictureOrAdd": "Click your picture or add an account",
|
||||
"addAccount": "Add an account",
|
||||
"show": "Show",
|
||||
"hide": "Hide",
|
||||
"signInWithGoogle": "Sign in with Google",
|
||||
"signInWithFacebook": "Sign in with Facebook",
|
||||
"enterCredentials": "Enter your credentials to continue",
|
||||
"rememberMe": "Remember me",
|
||||
"orContinueWith": "or continue with",
|
||||
"noAccount": "Don't have an account?",
|
||||
"signUp": "Sign up",
|
||||
"copyright": "© 2023 Your Company. All rights reserved.",
|
||||
"welcomeBack": "Welcome Back",
|
||||
"continueJourney": "Sign in to continue your journey with us",
|
||||
"emailWrong": "Please check email format",
|
||||
"passwordWrong": "Please check password format"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export const usePathname = () => {
|
|||
const locale = params?.locale as Locale;
|
||||
|
||||
// Remove locale prefix from pathname
|
||||
if (locale && nextPathname.startsWith(`/${locale}`)) {
|
||||
if (locale && nextPathname && nextPathname.startsWith(`/${locale}`)) {
|
||||
return nextPathname.substring(`/${locale}`.length) || "/";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@
|
|||
"home": "Ana Sayfa",
|
||||
"about": "Hakkında",
|
||||
"contact": "İletişim",
|
||||
"current": "Mevcut Dili"
|
||||
"current": "Mevcut Dili",
|
||||
"login": "Giriş Yap",
|
||||
"select": "Görev Seç"
|
||||
}
|
||||
},
|
||||
"LocaleLayout": {
|
||||
|
|
@ -41,6 +43,52 @@
|
|||
"signUp": "Kayıt ol",
|
||||
"copyright": "© 2023 Şirketiniz. Tüm hakları saklıdır.",
|
||||
"welcomeBack": "Tekrar Hoşgeldiniz",
|
||||
"continueJourney": "Yolculuğunuza devam etmek için giriş yapın"
|
||||
"continueJourney": "Yolculuğunuza devam etmek için giriş yapın",
|
||||
"emailWrong": "Emaili tekrar kontrol edin",
|
||||
"passwordWrong": "Parolayı tekrar kontrol edin"
|
||||
},
|
||||
"Select": {
|
||||
"title": "Seçeneğinizi Seçin",
|
||||
"description": "Devam etmek için lütfen aşağıdaki seçeneklerden birini seçin",
|
||||
"employee": "Çalışan",
|
||||
"staff": "Personel",
|
||||
"occupant_type": "İkamet Tipi",
|
||||
"uuid": "UUID",
|
||||
"department": "Departman",
|
||||
"name": "İsim",
|
||||
"code": "Kod",
|
||||
"company": "Şirket",
|
||||
"occupant": "Oturak",
|
||||
"occupant_code": "Oturak Kodu",
|
||||
"building": "Bina",
|
||||
"type": "Tip",
|
||||
"part_details": "Parça Detayları",
|
||||
"no": "No",
|
||||
"level": "Seviye",
|
||||
"status": "Durum",
|
||||
"livable": "Yaşanabilir",
|
||||
"not_livable": "Yaşanamaz",
|
||||
"selection": "Seçim",
|
||||
"id": "ID",
|
||||
"processing": "İşleniyor...",
|
||||
"continue": "Devam Et",
|
||||
"select_option": "Devam etmek için bir seçenek seçin",
|
||||
"recentLogins": "Son girişler",
|
||||
"clickPictureOrAdd": "Resminize tıklayın veya bir hesap ekleyin",
|
||||
"addAccount": "Hesap ekle",
|
||||
"show": "Göster",
|
||||
"hide": "Gizle",
|
||||
"signInWithGoogle": "Google ile giriş yap",
|
||||
"signInWithFacebook": "Facebook ile giriş yap",
|
||||
"enterCredentials": "Devam etmek için kimlik bilgilerinizi girin",
|
||||
"rememberMe": "Beni hatırla",
|
||||
"orContinueWith": "veya şununla devam et",
|
||||
"noAccount": "Hesabınız yok mu?",
|
||||
"signUp": "Kayıt ol",
|
||||
"copyright": " 2023 Şirketiniz. Tüm hakları saklıdır.",
|
||||
"welcomeBack": "Tekrar Hoş Geldiniz",
|
||||
"continueJourney": "Bizimle yolculuğunuza devam etmek için giriş yapın",
|
||||
"emailWrong": "Lütfen e-posta formatını kontrol edin",
|
||||
"passwordWrong": "Lütfen şifre formatını kontrol edin"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
interface FetcherRequest {
|
||||
url: string;
|
||||
isNoCache: boolean;
|
||||
|
|
@ -38,16 +40,23 @@ async function apiPostFetcher<T>({
|
|||
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: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" },
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
credentials: 'include'
|
||||
})
|
||||
if (response.status === 200) {
|
||||
const result = await response.json();
|
||||
return { success: true, data: result.data, pagination: result.pagination }
|
||||
}
|
||||
} catch (error) { }
|
||||
} catch (error) {
|
||||
console.error('API Post Fetcher error:', error);
|
||||
}
|
||||
return { success: false, data: null }
|
||||
}
|
||||
|
||||
|
|
@ -57,16 +66,37 @@ async function apiGetFetcher<T>({
|
|||
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: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" },
|
||||
headers,
|
||||
credentials: 'include'
|
||||
})
|
||||
console.log("apiGetFetcher status", await response.text());
|
||||
if (response.status === 200) {
|
||||
const result = await response.json();
|
||||
return { success: true, data: result.data }
|
||||
let responseText;
|
||||
try {
|
||||
responseText = await response.text();
|
||||
console.log("apiGetFetcher status", response.status, responseText.substring(0, 100));
|
||||
} catch (e) {
|
||||
console.error("Error reading response text", e);
|
||||
}
|
||||
} catch (error) { }
|
||||
if (response.status === 200) {
|
||||
let result;
|
||||
try {
|
||||
if (responseText) {
|
||||
result = JSON.parse(responseText);
|
||||
return { success: true, data: result.data };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing JSON response", e);
|
||||
}
|
||||
return { success: false, data: null };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API Get Fetcher error:', error);
|
||||
}
|
||||
return { success: false, data: null }
|
||||
}
|
||||
|
||||
|
|
@ -76,18 +106,23 @@ async function apiDeleteFetcher<T>({
|
|||
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: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" },
|
||||
headers,
|
||||
credentials: 'include'
|
||||
})
|
||||
if (response.status === 200) {
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
}
|
||||
return { success: true, data: result.data }
|
||||
}
|
||||
} catch (error) { }
|
||||
} catch (error) {
|
||||
console.error('API Delete Fetcher error:', error);
|
||||
}
|
||||
return { success: false, data: null }
|
||||
}
|
||||
|
||||
|
|
@ -98,10 +133,15 @@ async function apiPutFetcher<T>({
|
|||
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: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" },
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
credentials: 'include'
|
||||
})
|
||||
if (response.status === 200) {
|
||||
const result = await response.json();
|
||||
|
|
@ -121,10 +161,15 @@ async function apiPatchFetcher<T>({
|
|||
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: { "Content-Type": "application/json", "no-cache": isNoCache ? "true" : "false" },
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
credentials: 'include'
|
||||
})
|
||||
if (response.status === 200) {
|
||||
const result = await response.json();
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"title": "Welcome Ninja"
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"title": "Hoşgeldiniz Ninja"
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ services:
|
|||
dockerfile: ServicesApi/Dockerfile
|
||||
target: builder
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "8001:8001"
|
||||
env_file:
|
||||
- api_env.env
|
||||
environment:
|
||||
|
|
|
|||
Loading…
Reference in New Issue