updated login page with qwen
This commit is contained in:
@@ -31,8 +31,8 @@ export class AuthController {
|
||||
@Post('select')
|
||||
@HttpCode(200)
|
||||
@UseGuards(AuthControlGuard)
|
||||
async select(@Body() query: userSelectValidator) {
|
||||
return { message: 'Logout successful' };
|
||||
async select(@Body() query: userSelectValidator, @Req() req: Request) {
|
||||
return await this.authService.select(query, req);
|
||||
}
|
||||
|
||||
@Post('/password/create')
|
||||
|
||||
@@ -32,8 +32,8 @@ export class AuthService {
|
||||
return await this.logoutService.run(dto);
|
||||
}
|
||||
|
||||
async select(dto: userSelectValidator) {
|
||||
return await this.selectService.run(dto);
|
||||
async select(dto: userSelectValidator, req: Request) {
|
||||
return await this.selectService.run(dto, req);
|
||||
}
|
||||
|
||||
async createPassword(dto: any) {
|
||||
|
||||
@@ -18,9 +18,9 @@ export class LoginService {
|
||||
where: { email: dto.accessKey },
|
||||
});
|
||||
|
||||
// if (foundUser.password_token) {
|
||||
// throw new Error('Password need to be set first');
|
||||
// }
|
||||
if (foundUser.password_token) {
|
||||
throw new Error('Password need to be set first');
|
||||
}
|
||||
|
||||
const isPasswordValid = this.passHandlers.check_password(
|
||||
foundUser.uu_id,
|
||||
@@ -28,14 +28,13 @@ export class LoginService {
|
||||
foundUser.hash_password,
|
||||
);
|
||||
|
||||
// if (!isPasswordValid) {
|
||||
// throw new Error('Invalid password');
|
||||
// }
|
||||
if (!isPasswordValid) {
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
|
||||
const foundPerson = await this.prisma.people.findFirstOrThrow({
|
||||
where: { id: foundUser.id },
|
||||
});
|
||||
|
||||
const redisData = AuthTokenSchema.parse({
|
||||
people: foundPerson,
|
||||
users: foundUser,
|
||||
@@ -43,8 +42,8 @@ export class LoginService {
|
||||
person_id: foundPerson.id,
|
||||
person_name: foundPerson.firstname,
|
||||
},
|
||||
selectionList: [],
|
||||
});
|
||||
|
||||
const accessToken = await this.redis.setLoginToRedis(
|
||||
redisData,
|
||||
foundUser.uu_id,
|
||||
|
||||
@@ -29,5 +29,4 @@ export class VerifyOtpService {
|
||||
// // controller veya resolver içinden
|
||||
// const { secret, otpauthUrl } = authService.generate2FASecret('mehmet');
|
||||
// const qrCodeImage = await authService.generateQRCode(otpauthUrl);
|
||||
|
||||
// // qrCodeImage → frontend’e gönder, <img src="data:image/png;base64,..."> diye gösterilebilir
|
||||
|
||||
@@ -1,9 +1,124 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
BadRequestException,
|
||||
UnauthorizedException,
|
||||
NotAcceptableException,
|
||||
} from '@nestjs/common';
|
||||
import { userSelectValidator } from '@/src/auth/select/dtoValidator';
|
||||
import { RedisHandlers } from '@/src/utils/auth/redis_handlers';
|
||||
import {
|
||||
EmployeeTokenSchema,
|
||||
OccupantTokenSchema,
|
||||
TokenDictInterface,
|
||||
UserType,
|
||||
} from '@/src/types/auth/token';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class SelectService {
|
||||
async run(dto: userSelectValidator) {
|
||||
return dto;
|
||||
constructor(
|
||||
private readonly redis: RedisHandlers,
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
async run(dto: userSelectValidator, req: Request) {
|
||||
const accessObject = await this.redis.getLoginFromRedis(req);
|
||||
if (!accessObject) {
|
||||
throw new UnauthorizedException(
|
||||
'Authorization failed. Please login to continue',
|
||||
);
|
||||
}
|
||||
const accessToken = accessObject.key.split(':')[1];
|
||||
console.log('accessToken', accessToken);
|
||||
|
||||
const userType = accessObject.value.users.user_type;
|
||||
if (userType === 'employee') {
|
||||
const employee = await this.prisma.employees.findFirstOrThrow({
|
||||
where: { uu_id: dto.selected_uu_id },
|
||||
});
|
||||
const staff = await this.prisma.staff.findFirstOrThrow({
|
||||
where: { id: employee.staff_id },
|
||||
});
|
||||
const duties = await this.prisma.duties.findFirstOrThrow({
|
||||
where: { id: staff.duties_id },
|
||||
});
|
||||
const department = await this.prisma.departments.findFirstOrThrow({
|
||||
where: { id: duties.department_id },
|
||||
});
|
||||
const duty = await this.prisma.duty.findFirstOrThrow({
|
||||
where: { id: duties.duties_id },
|
||||
});
|
||||
const company = await this.prisma.companies.findFirstOrThrow({
|
||||
where: { id: duties.company_id },
|
||||
});
|
||||
|
||||
const employeeToken = EmployeeTokenSchema.parse({
|
||||
company: company,
|
||||
department: department,
|
||||
duty: duty,
|
||||
employee: employee,
|
||||
staff: staff,
|
||||
menu: null,
|
||||
pages: null,
|
||||
config: null,
|
||||
caches: null,
|
||||
selection: null,
|
||||
functionsRetriever: staff.function_retriever,
|
||||
kind: UserType.employee,
|
||||
});
|
||||
|
||||
const tokenSelect = await this.redis.setSelectToRedis(
|
||||
accessToken,
|
||||
employeeToken,
|
||||
accessObject.value.users.uu_id,
|
||||
dto.selected_uu_id,
|
||||
);
|
||||
|
||||
return {
|
||||
message: 'Select successful',
|
||||
token: tokenSelect,
|
||||
};
|
||||
} else if (userType === 'occupant') {
|
||||
const livingSpace = await this.prisma.build_living_space.findFirstOrThrow(
|
||||
{ where: { uu_id: dto.selected_uu_id } },
|
||||
);
|
||||
const occupantType = await this.prisma.occupant_types.findFirstOrThrow({
|
||||
where: { id: livingSpace.occupant_type_id },
|
||||
});
|
||||
const part = await this.prisma.build_parts.findFirstOrThrow({
|
||||
where: { id: livingSpace.build_parts_id },
|
||||
});
|
||||
const build = await this.prisma.build.findFirstOrThrow({
|
||||
where: { id: part.build_id },
|
||||
});
|
||||
const company = await this.prisma.companies.findFirstOrThrow({
|
||||
where: { uu_id: accessObject.value.users.related_company },
|
||||
});
|
||||
const occupantToken = OccupantTokenSchema.parse({
|
||||
livingSpace: livingSpace,
|
||||
occupant: occupantType,
|
||||
build: build,
|
||||
part: part,
|
||||
company: company,
|
||||
menu: null,
|
||||
pages: null,
|
||||
config: null,
|
||||
caches: null,
|
||||
selection: null,
|
||||
functionsRetriever: occupantType.function_retriever,
|
||||
kind: UserType.occupant,
|
||||
});
|
||||
const tokenSelect = await this.redis.setSelectToRedis(
|
||||
accessToken,
|
||||
occupantToken,
|
||||
accessObject.value.users.uu_id,
|
||||
dto.selected_uu_id,
|
||||
);
|
||||
return {
|
||||
message: 'Select successful',
|
||||
token: tokenSelect,
|
||||
};
|
||||
} else {
|
||||
throw new NotAcceptableException('Invalid user type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,6 @@ export class AuthControlGuard implements CanActivate {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const accessToken = this.cacheService.mergeLoginKey(req);
|
||||
console.log('AuthControlGuard', accessToken);
|
||||
// const hasAccess = accessObject.permissions?.some(
|
||||
// (p: any) => p.method === method && p.url === path,
|
||||
// );
|
||||
|
||||
// if (!hasAccess) {
|
||||
// throw new ForbiddenException('Access denied to this route');
|
||||
// }
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -32,18 +24,11 @@ export class EndpointControlGuard implements CanActivate {
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const selectToken = this.cacheService.mergeSelectKey(req);
|
||||
const method = req.method;
|
||||
const path = req.route?.path;
|
||||
console.log('EndpointControlGuard', selectToken, method, path);
|
||||
// const hasAccess = accessObject.permissions?.some(
|
||||
// (p: any) => p.method === method && p.url === path,
|
||||
// );
|
||||
|
||||
// if (!hasAccess) {
|
||||
// throw new ForbiddenException('Access denied to this route');
|
||||
// }
|
||||
|
||||
// const selectToken = this.cacheService.mergeSelectKey(req);
|
||||
// const method = req.method;
|
||||
// const path = req.route?.path;
|
||||
const accessObject = await this.cacheService.getSelectFromRedis(req);
|
||||
console.log('EndpointControlGuard', accessObject);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const AuthTokenSchema = z.object({
|
||||
father_name: z.string(),
|
||||
mother_name: z.string(),
|
||||
country_code: z.string(),
|
||||
national_identity_id: z.string(),
|
||||
// national_identity_id: z.string(),
|
||||
birth_place: z.string(),
|
||||
birth_date: z.date(),
|
||||
tax_no: z.string(),
|
||||
@@ -52,10 +52,11 @@ export const AuthTokenSchema = z.object({
|
||||
users: z.object({
|
||||
user_tag: z.string(),
|
||||
email: z.string(),
|
||||
user_type: z.string(),
|
||||
phone_number: z.string(),
|
||||
via: z.string(),
|
||||
avatar: z.string(),
|
||||
hash_password: z.string(),
|
||||
// hash_password: z.string(),
|
||||
password_token: z.string(),
|
||||
remember_me: z.boolean(),
|
||||
password_expires_day: z.number(),
|
||||
@@ -85,33 +86,173 @@ export const AuthTokenSchema = z.object({
|
||||
default_language: z.string(),
|
||||
}),
|
||||
credentials: CredentialsSchema,
|
||||
selectionList: z.array(z.any()).optional().default([]),
|
||||
});
|
||||
|
||||
export type AuthToken = z.infer<typeof AuthTokenSchema>;
|
||||
|
||||
export const EmployeeTokenSchema = z.object({
|
||||
company: z.object({
|
||||
id: z.number(),
|
||||
uu_id: z.string(),
|
||||
formal_name: z.string(),
|
||||
company_type: z.string(),
|
||||
commercial_type: z.string(),
|
||||
tax_no: z.string(),
|
||||
public_name: z.string(),
|
||||
company_tag: z.string(),
|
||||
default_lang_type: z.string(),
|
||||
default_money_type: z.string(),
|
||||
is_commercial: z.boolean(),
|
||||
is_blacklist: z.boolean(),
|
||||
parent_id: z.number().nullable(),
|
||||
workplace_no: z.string().nullable(),
|
||||
official_address_id: z.number().nullable(),
|
||||
official_address_uu_id: z.string().nullable(),
|
||||
top_responsible_company_id: z.number().nullable(),
|
||||
top_responsible_company_uu_id: z.string().nullable(),
|
||||
ref_id: z.string().nullable(),
|
||||
replication_id: z.number(),
|
||||
cryp_uu_id: z.string().nullable(),
|
||||
created_credentials_token: z.string().nullable(),
|
||||
updated_credentials_token: z.string().nullable(),
|
||||
confirmed_credentials_token: z.string().nullable(),
|
||||
is_confirmed: z.boolean(),
|
||||
deleted: z.boolean(),
|
||||
active: z.boolean(),
|
||||
is_notification_send: z.boolean(),
|
||||
is_email_send: z.boolean(),
|
||||
expiry_starts: z.date(),
|
||||
expiry_ends: z.date(),
|
||||
created_at: z.date(),
|
||||
updated_at: z.date(),
|
||||
ref_int: z.number().nullable(),
|
||||
}),
|
||||
department: z.object({
|
||||
id: z.number(),
|
||||
uu_id: z.string(),
|
||||
parent_department_id: z.number().nullable(),
|
||||
department_code: z.string(),
|
||||
department_name: z.string(),
|
||||
department_description: z.string(),
|
||||
company_id: z.number(),
|
||||
company_uu_id: z.string(),
|
||||
ref_id: z.string().nullable(),
|
||||
replication_id: z.number(),
|
||||
cryp_uu_id: z.string().nullable(),
|
||||
created_credentials_token: z.string().nullable(),
|
||||
updated_credentials_token: z.string().nullable(),
|
||||
confirmed_credentials_token: z.string().nullable(),
|
||||
is_confirmed: z.boolean(),
|
||||
deleted: z.boolean(),
|
||||
active: z.boolean(),
|
||||
is_notification_send: z.boolean(),
|
||||
is_email_send: z.boolean(),
|
||||
expiry_starts: z.date(),
|
||||
expiry_ends: z.date(),
|
||||
created_at: z.date(),
|
||||
updated_at: z.date(),
|
||||
ref_int: z.number().nullable(),
|
||||
}),
|
||||
duty: z.object({
|
||||
id: z.number(),
|
||||
uu_id: z.string(),
|
||||
duty_name: z.string(),
|
||||
duty_code: z.string(),
|
||||
duty_description: z.string(),
|
||||
ref_id: z.string().nullable(),
|
||||
replication_id: z.number(),
|
||||
cryp_uu_id: z.string().nullable(),
|
||||
created_credentials_token: z.string().nullable(),
|
||||
updated_credentials_token: z.string().nullable(),
|
||||
confirmed_credentials_token: z.string().nullable(),
|
||||
is_confirmed: z.boolean(),
|
||||
deleted: z.boolean(),
|
||||
active: z.boolean(),
|
||||
is_notification_send: z.boolean(),
|
||||
is_email_send: z.boolean(),
|
||||
expiry_starts: z.date(),
|
||||
expiry_ends: z.date(),
|
||||
created_at: z.date(),
|
||||
updated_at: z.date(),
|
||||
ref_int: z.number().nullable(),
|
||||
}),
|
||||
employee: z.object({
|
||||
id: z.number(),
|
||||
uu_id: z.string(),
|
||||
staff_id: z.number(),
|
||||
staff_uu_id: z.string(),
|
||||
people_id: z.number(),
|
||||
people_uu_id: z.string(),
|
||||
ref_id: z.string().nullable(),
|
||||
replication_id: z.number(),
|
||||
cryp_uu_id: z.string().nullable(),
|
||||
created_credentials_token: z.string().nullable(),
|
||||
updated_credentials_token: z.string().nullable(),
|
||||
confirmed_credentials_token: z.string().nullable(),
|
||||
is_confirmed: z.boolean(),
|
||||
deleted: z.boolean(),
|
||||
active: z.boolean(),
|
||||
is_notification_send: z.boolean(),
|
||||
is_email_send: z.boolean(),
|
||||
expiry_starts: z.date(),
|
||||
expiry_ends: z.date(),
|
||||
created_at: z.date(),
|
||||
updated_at: z.date(),
|
||||
ref_int: z.number().nullable(),
|
||||
}),
|
||||
staff: z.object({
|
||||
id: z.number(),
|
||||
uu_id: z.string(),
|
||||
staff_description: z.string(),
|
||||
staff_name: z.string(),
|
||||
staff_code: z.string(),
|
||||
duties_id: z.number(),
|
||||
duties_uu_id: z.string(),
|
||||
function_retriever: z.string().nullable(),
|
||||
ref_id: z.string().nullable(),
|
||||
replication_id: z.number(),
|
||||
cryp_uu_id: z.string().nullable(),
|
||||
created_credentials_token: z.string().nullable(),
|
||||
updated_credentials_token: z.string().nullable(),
|
||||
confirmed_credentials_token: z.string().nullable(),
|
||||
is_confirmed: z.boolean(),
|
||||
deleted: z.boolean(),
|
||||
active: z.boolean(),
|
||||
is_notification_send: z.boolean(),
|
||||
is_email_send: z.boolean(),
|
||||
expiry_starts: z.date(),
|
||||
expiry_ends: z.date(),
|
||||
created_at: z.date(),
|
||||
updated_at: z.date(),
|
||||
ref_int: z.number().nullable(),
|
||||
}),
|
||||
|
||||
menu: z.array(z.object({})).nullable(),
|
||||
pages: z.array(z.string()).nullable(),
|
||||
// config: z.record(z.string(), z.unknown()).nullable(),
|
||||
// caches: z.record(z.string(), z.unknown()).nullable(),
|
||||
|
||||
selection: z.record(z.string(), z.unknown()).nullable(),
|
||||
functionsRetriever: z.string(),
|
||||
companies: z.object({}),
|
||||
department: z.object({}),
|
||||
duties: z.object({}),
|
||||
employee: z.object({}),
|
||||
staffs: z.object({}),
|
||||
reachable_event_codes: z.array(z.object({})),
|
||||
reachable_app_codes: z.array(z.object({})),
|
||||
kind: z.literal(UserType.employee),
|
||||
});
|
||||
|
||||
export const OccupantTokenSchema = z.object({
|
||||
functionsRetriever: z.string(),
|
||||
livingSpace: z.object({}),
|
||||
occupantType: z.object({}),
|
||||
occupant: z.object({}),
|
||||
build: z.object({}),
|
||||
buildPart: z.object({}),
|
||||
responsibleCompany: z.object({}).optional(),
|
||||
responsibleEmployee: z.object({}).optional(),
|
||||
part: z.object({}),
|
||||
company: z.object({}).optional(),
|
||||
|
||||
menu: z.array(z.object({})).nullable(),
|
||||
pages: z.array(z.string()).nullable(),
|
||||
// config: z.record(z.string(), z.unknown()).nullable(),
|
||||
// caches: z.record(z.string(), z.unknown()).nullable(),
|
||||
|
||||
selection: z.record(z.string(), z.unknown()).nullable(),
|
||||
functionsRetriever: z.string(),
|
||||
kind: z.literal(UserType.occupant),
|
||||
reachable_event_codes: z.array(z.object({})),
|
||||
reachable_app_codes: z.array(z.object({})),
|
||||
});
|
||||
|
||||
export const TokenDictTypes = z.discriminatedUnion('kind', [
|
||||
|
||||
@@ -2,10 +2,9 @@ import {
|
||||
TokenDictTypes,
|
||||
TokenDictInterface,
|
||||
AuthToken,
|
||||
UserType,
|
||||
AuthTokenSchema,
|
||||
} from '@/src/types/auth/token';
|
||||
import { CacheService } from '@/src/cache.service';
|
||||
import { users } from '@prisma/client';
|
||||
import { PasswordHandlers } from './login_handler';
|
||||
import { Injectable, ForbiddenException } from '@nestjs/common';
|
||||
|
||||
@@ -22,6 +21,7 @@ interface SelectFromRedis {
|
||||
@Injectable()
|
||||
export class RedisHandlers {
|
||||
AUTH_TOKEN = 'AUTH_TOKEN';
|
||||
SELECT_TOKEN = 'SELECT_TOKEN';
|
||||
constructor(
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly passwordService: PasswordHandlers,
|
||||
@@ -32,10 +32,10 @@ export class RedisHandlers {
|
||||
* Format: AUTH_TOKEN:token:token:UUID or AUTH_TOKEN:token:token:*:*
|
||||
this.AUTH_TOKEN:token:token:UUID:UUID
|
||||
*/
|
||||
private validateRedisKey(redisKey: string): boolean {
|
||||
if (!redisKey.startsWith(this.AUTH_TOKEN + ':')) {
|
||||
private validateRedisKey(redisKey: string, type: string): boolean {
|
||||
if (!redisKey.startsWith(type + ':')) {
|
||||
throw new ForbiddenException(
|
||||
`Invalid Redis key format. Must start with ${this.AUTH_TOKEN}:`,
|
||||
`Invalid Redis key format. Must start with ${type}:`,
|
||||
);
|
||||
}
|
||||
const colonCount = (redisKey.match(/:/g) || []).length;
|
||||
@@ -53,7 +53,7 @@ export class RedisHandlers {
|
||||
throw new ForbiddenException('Access token header is missing');
|
||||
}
|
||||
const mergedRedisKey = `${this.AUTH_TOKEN}:${acsToken}:${acsToken}:*:*`;
|
||||
this.validateRedisKey(mergedRedisKey);
|
||||
this.validateRedisKey(mergedRedisKey, this.AUTH_TOKEN);
|
||||
return mergedRedisKey;
|
||||
}
|
||||
|
||||
@@ -66,8 +66,8 @@ export class RedisHandlers {
|
||||
if (!slcToken) {
|
||||
throw new ForbiddenException('Select token header is missing');
|
||||
}
|
||||
const mergedRedisKey = `${this.AUTH_TOKEN}:${acsToken}:${slcToken}:*:*`;
|
||||
this.validateRedisKey(mergedRedisKey);
|
||||
const mergedRedisKey = `${this.SELECT_TOKEN}:${acsToken}:${slcToken}:*:*`;
|
||||
this.validateRedisKey(mergedRedisKey, this.SELECT_TOKEN);
|
||||
return mergedRedisKey;
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ export class RedisHandlers {
|
||||
return this.passwordService.generateAccessToken();
|
||||
}
|
||||
|
||||
private async scanKeys(pattern: string): Promise<string[]> {
|
||||
this.validateRedisKey(pattern);
|
||||
private async scanKeys(pattern: string, type: string): Promise<string[]> {
|
||||
this.validateRedisKey(pattern, type);
|
||||
const client = (this.cacheService as any).client;
|
||||
if (!client) throw new Error('Redis client not available');
|
||||
|
||||
@@ -103,7 +103,7 @@ export class RedisHandlers {
|
||||
async getLoginFromRedis(req: Request): Promise<LoginFromRedis | null> {
|
||||
const mergedKey = this.mergeLoginKey(req);
|
||||
if (mergedKey.includes('*')) {
|
||||
const keys = await this.scanKeys(mergedKey);
|
||||
const keys = await this.scanKeys(mergedKey, this.AUTH_TOKEN);
|
||||
if (keys.length === 0) {
|
||||
throw new ForbiddenException('Authorization failed - No matching keys');
|
||||
}
|
||||
@@ -122,13 +122,15 @@ export class RedisHandlers {
|
||||
}
|
||||
|
||||
const value = await this.cacheService.get(mergedKey);
|
||||
return value ? { key: mergedKey, value } : null;
|
||||
return value
|
||||
? { key: mergedKey, value: AuthTokenSchema.parse(value) }
|
||||
: null;
|
||||
}
|
||||
|
||||
async getSelectFromRedis(req: Request): Promise<SelectFromRedis | null> {
|
||||
const mergedKey = this.mergeSelectKey(req);
|
||||
if (mergedKey.includes('*')) {
|
||||
const keys = await this.scanKeys(mergedKey);
|
||||
const keys = await this.scanKeys(mergedKey, this.SELECT_TOKEN);
|
||||
|
||||
if (keys.length === 0) {
|
||||
throw new ForbiddenException(
|
||||
@@ -147,7 +149,9 @@ export class RedisHandlers {
|
||||
}
|
||||
|
||||
const value = await this.cacheService.get(mergedKey);
|
||||
return value ? { key: mergedKey, value } : null;
|
||||
return value
|
||||
? { key: mergedKey, value: TokenDictTypes.parse(value) }
|
||||
: null;
|
||||
}
|
||||
|
||||
async deleteLoginFromRedis(req: Request): Promise<any> {
|
||||
@@ -186,7 +190,7 @@ export class RedisHandlers {
|
||||
livingUUID: string,
|
||||
): Promise<any> {
|
||||
const selectToken = this.generateSelectToken(accessToken, userUUID);
|
||||
const redisKey = `${this.AUTH_TOKEN}:${accessToken}:${selectToken}:${userUUID}:${livingUUID}`;
|
||||
const redisKey = `${this.SELECT_TOKEN}:${accessToken}:${selectToken}:${userUUID}:${livingUUID}`;
|
||||
await this.cacheService.set_with_ttl(redisKey, token, 60 * 30);
|
||||
return selectToken;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user