diff --git a/package.json b/package.json index c881f6b..43b473e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@alicloud/pop-core": "^1.7.10", + "@keycloak/keycloak-admin-client": "^18.0.2", "@nestjs-modules/ioredis": "^1.0.0", "@nestjs/common": "^8.0.0", "@nestjs/core": "^8.0.0", @@ -36,6 +37,7 @@ "ioredis": "^5.0.5", "jsonwebtoken": "^8.5.1", "moment": "^2.29.3", + "node-keycloak": "^0.1.5", "openid-client": "^5.1.6", "passport": "^0.5.2", "passport-jwt": "^4.0.0", diff --git a/src/app.controller.ts b/src/app.controller.ts index ce0b31b..88a01ec 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,12 +1,17 @@ import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; +import { UserService } from './services'; @Controller() export class AppController { - constructor(private readonly appService: AppService) {} + constructor( + private readonly appService: AppService, + private readonly userService: UserService, + ) {} @Get('checkHealth') async checkHealth(): Promise { + // await this.userService.generateUserDepartment(); return this.appService.checkHealth(); } } diff --git a/src/app.module.ts b/src/app.module.ts index d2b1593..3eb67d3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,6 +3,7 @@ import { RedisModule } from '@nestjs-modules/ioredis'; import { ScheduleModule } from '@nestjs/schedule'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { @@ -13,9 +14,13 @@ import { UserController, InformController, } from './controllers'; -import { jwtModuleOptions, redisModuleOptions } from './modules'; +import { + jwtModuleOptions, + redisModuleOptions, + typeOrmOptions, +} from './modules'; import { TimeSheetSocket } from './sockets'; -import { TimeSheetSchedule } from './schedules'; +import { TimeSheetSchedule, KeyCloakSchedule } from './schedules'; import { AttendanceService, AuthService, @@ -23,26 +28,30 @@ import { ReportService, UserService, InformService, + TimeSheetService, + UserDepartmentService, } from './services'; import { JwtStrategy, WsGuard } from './strategys'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { join } from 'path'; -import config from '@config/config'; -import { UserTimesheet } from './entities/timesheet.enetity'; +import { + DataResource, + DataDepartment, + UserTimesheet, + UserDepartment, +} from './entities'; + @Module({ imports: [ PassportModule, JwtModule.register(jwtModuleOptions), RedisModule.forRoot(redisModuleOptions), ScheduleModule.forRoot(), - TypeOrmModule.forRoot({ - type: 'postgres', - ...config.postgresql, - entities: [join(__dirname, '**', '*.entity.{ts,js}')], - migrationsTableName: 'migration', - migrations: ['src/migration/*.ts'], - }), - TypeOrmModule.forFeature([UserTimesheet]), + TypeOrmModule.forRoot(typeOrmOptions), + TypeOrmModule.forFeature([ + DataResource, + DataDepartment, + UserTimesheet, + UserDepartment, + ]), ], controllers: [ AppController, @@ -63,8 +72,11 @@ import { UserTimesheet } from './entities/timesheet.enetity'; AuthService, UserService, InformService, + TimeSheetService, + UserDepartmentService, TimeSheetSocket, TimeSheetSchedule, + KeyCloakSchedule, ], exports: [AuthService], }) diff --git a/src/config/config.ts b/src/config/config.ts index 4564d83..8de8f7e 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,4 +1,5 @@ import { ServerEnvironment } from '@constants/server'; +import { GrantTypes } from '@keycloak/keycloak-admin-client/lib/utils/auth'; import * as config from 'config'; interface IConfig { server: { @@ -31,6 +32,7 @@ interface IConfig { attendanceRule: string; reportRule: string; saveTimeSheetRule: string; + keyCloakAuthRule: string; }; redis: { host: string; @@ -46,6 +48,10 @@ interface IConfig { scope: string; grantType: string; logoutRedirectUri: string; + clientBaseUrl: string; + username: string; + password: string; + clientGrantType: GrantTypes; }; jwt: { secret: string; @@ -89,26 +95,13 @@ export default { code: config.get('smsTemplate.code'), signName: config.get('smsTemplate.signName'), }, - job: { - attendanceRule: config.get('job.attendanceRule'), - reportRule: config.get('job.reportRule'), - saveTimeSheetRule: config.get('job.saveTimeSheetRule'), - }, + job: config.get('job'), redis: { host: config.get('redis.host'), port: config.get('redis.port'), password: config.get('redis.password'), }, - keycloak: { - realm: config.get('keycloak.realm'), - issuer: config.get('keycloak.issuer'), - clientId: config.get('keycloak.clientId'), - redirectUri: config.get('keycloak.redirectUri'), - scope: config.get('keycloak.scope'), - clientSecret: config.get('keycloak.clientSecret'), - grantType: config.get('keycloak.grantType'), - logoutRedirectUri: config.get('keycloak.logoutRedirectUri'), - }, + keycloak: config.get('keycloak'), jwt: { secret: config.get('jwt.secret'), expiresIn: config.get('jwt.expiresIn'), diff --git a/src/constants/dingTalk.ts b/src/constants/dingTalk.ts index 7389cc2..9b066fe 100644 --- a/src/constants/dingTalk.ts +++ b/src/constants/dingTalk.ts @@ -16,6 +16,7 @@ export enum LeaveDurationUnitType { * 未提交日志 X 6 * 加班 J 7 * 迟到 L 8 + * 打卡时间 A 9 */ export enum AttendanceState { 'O' = 1, @@ -26,6 +27,7 @@ export enum AttendanceState { 'X' = 6, 'J' = 7, 'L' = 8, + 'A' = 9, } export enum AttendanceCheckType { diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 69b9090..a05a131 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -7,23 +7,23 @@ import { UseGuards, Request, } from '@nestjs/common'; -import KcClient from '@utils/kcClient'; import { AuthService } from '@services/auth.service'; import { AuthGuard } from '@nestjs/passport'; import { NestRes } from '@interfaces/nestbase'; +import NodeKeycloak from 'node-keycloak'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Get('/url') async getAuthUrl() { - return await KcClient.client.authorizationUrl(); + return await NodeKeycloak.authorizationUrl(); } @Post('/authentication') async authentication(@Body() authDto: IAuthDto) { - const { code, state, session_state } = authDto; - return await this.authService.signin(code, state, session_state); + const { code, session_state } = authDto; + return await this.authService.signin(code, session_state); } @UseGuards(AuthGuard('jwt')) diff --git a/src/controllers/dingTalk.controller.ts b/src/controllers/dingTalk.controller.ts index 90bd8cb..56064a9 100644 --- a/src/controllers/dingTalk.controller.ts +++ b/src/controllers/dingTalk.controller.ts @@ -2,7 +2,6 @@ import config from '@config/config'; import FileData from '@core/files.data'; import { ICreateReportDto, - IGetReportTemplateByNameDto, IUserCreateDto, IUserUpdateDto, } from '@dtos/dingTlak'; @@ -144,7 +143,7 @@ export class DingTalkController { async createReport(@Body() Body: ICreateReportDto, @Request() req: NestRes) { const templeDetail = await this.dingTalkService.getReportTemplateByName({ template_name: 'TIMESHEET', - userid: req.user.dingTalkUserId, + userid: req.user.dingUserId, }); const contents = []; contents[0] = { @@ -197,7 +196,7 @@ export class DingTalkController { to_chat: false, to_cids: [config.dingTalk.conversationId], dd_from: 'fenglin', - userid: req.user.dingTalkUserId, + userid: req.user.dingUserId, }; templeDetail.result?.default_receivers && (params.to_userids = templeDetail.result.default_receivers.map((item) => { @@ -213,14 +212,14 @@ export class DingTalkController { @Get('/getReportTemplateByName') async getReportTemplateByName(@Request() req: NestRes) { - const { dingTalkUserId } = req.user; + const { dingUserId } = req.user; const _timesheet = await this.redis.get('timesheets'); const datas = JSON.parse(_timesheet || '[]'); - const userTimeSheet = datas.find((x) => x.userid === dingTalkUserId); + const userTimeSheet = datas.find((x) => x.userid === dingUserId); const result = await this.dingTalkService.getReportTemplateByName({ template_name: 'TIMESHEET', - userid: dingTalkUserId, + userid: dingUserId, }); result.result.value = userTimeSheet?.value; return result.result; @@ -232,7 +231,7 @@ export class DingTalkController { start_time: moment().startOf('day').format('x'), end_time: moment().endOf('day').format('x'), template_name: 'TIMESHEET', - userid: req.user.dingTalkUserId, + userid: req.user.dingUserId, cursor: 0, size: 1, }); diff --git a/src/controllers/timesheet.controller.ts b/src/controllers/timesheet.controller.ts index a2c4d06..02c96b8 100644 --- a/src/controllers/timesheet.controller.ts +++ b/src/controllers/timesheet.controller.ts @@ -2,13 +2,17 @@ import { Body, Controller, Get, Param, Put, UseGuards } from '@nestjs/common'; import FileData from '@core/files.data'; import { ITimeSheet } from '@interfaces/timesheet'; import { InjectRedis, Redis } from '@nestjs-modules/ioredis'; -import { AuthGuard } from '@nestjs/passport'; import * as moment from 'moment'; +import { AuthGuard } from '@nestjs/passport'; +import { TimeSheetService } from '@services/timesheet.service'; @UseGuards(AuthGuard('jwt')) @Controller('timesheet') export class TimeSheetController { - constructor(@InjectRedis() private readonly redis: Redis) {} + constructor( + @InjectRedis() private readonly redis: Redis, + private readonly timesheetService: TimeSheetService, + ) {} @Get('/get/:dept_name/:date') async getTimesheet( @Param('dept_name') dept_name: string, @@ -23,8 +27,7 @@ export class TimeSheetController { datas = JSON.parse(_timesheet || '[]'); } else { try { - const result = await FileData.readTimeSheet(date); - datas = JSON.parse(result).users; + datas = await this.timesheetService.getTimeSheetByDate(date); } catch (err) {} } const timeSheetData = users diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 0c11fd9..47a55ee 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -1,12 +1,26 @@ import { NestRes } from '@interfaces/nestbase'; -import { Controller, Get, UseGuards, Request } from '@nestjs/common'; +import { + Controller, + Get, + UseGuards, + Request, + Param, + Put, +} from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { UserService } from '@services/user.service'; +import FileData from '@core/files.data'; +import * as moment from 'moment'; +import { UserDepartmentService } from '@services/department.service'; +import { IUserMemberDto } from '@dtos/user'; @UseGuards(AuthGuard('jwt')) @Controller('user') export class UserController { - constructor(private readonly userService: UserService) {} + constructor( + private readonly userService: UserService, + private readonly userDepartmentService: UserDepartmentService, + ) {} @Get('today') async getTodayInfo(@Request() req: NestRes) { return { @@ -16,4 +30,71 @@ export class UserController { ), }; } + + @Get('members') + async getUserMembers(@Request() req: NestRes): Promise { + const departmentMembers = + await this.userDepartmentService.getDepartmentMembers( + req.user.departmentIds, + ); + return departmentMembers.map((x) => { + return { + username: x.username, + avatar: x.attributes.avatar && x.attributes.avatar[0], + }; + }); + } + + @Get('attendance/:month') + async getUserAttendance( + @Param('month') month: string, + @Request() req: NestRes, + ) { + if ( + moment().diff(moment(month), 'months') === 1 || + moment().format('YYYY-MM') === moment(month).format('YYYY-MM') + ) { + const _month = moment(month).format('YYYY-MM'); + const dingUserId = req.user.dingUserId; + const dingdingAttendances = await FileData.readAttendances(_month); + const customAttendances = await FileData.readCustomAttendances(_month); + const dAttendances = dingdingAttendances.find((x) => x.id === dingUserId); + const cAttendances = customAttendances.find((x) => x.id === dingUserId); + const attendances = dAttendances?.attendances.map((x, index) => { + const value = cAttendances.attendances[index]; + if (value !== null) x = value; + return x; + }); + return attendances || []; + } + return []; + } + + @Put('resource') + async updateUser() { + const users = await this.userService.getUsers(); + for (const user of users) { + let resourceIds = ''; + if (user.attributes['departmentids'].includes('1')) { + resourceIds = '1,2'; + } else if ( + user.attributes['departmentids'].includes('2') || + user.attributes['departmentids'].includes('3') + ) { + resourceIds = '1'; + } else if ( + user.attributes['departmentids'].includes('4') || + user.attributes['departmentids'].includes('5') + ) { + resourceIds = '1,3'; + } else { + resourceIds = '1,2,3'; + } + // await this.userService.updateUserResource(user.id, resourceIds); + } + } + // @Get('users') + // async getUsers() { + + // } } diff --git a/src/dtos/user.ts b/src/dtos/user.ts new file mode 100644 index 0000000..8bbe44a --- /dev/null +++ b/src/dtos/user.ts @@ -0,0 +1,4 @@ +export interface IUserMemberDto { + username: string; + avatar: string; +} diff --git a/src/entities/data.department.entity.ts b/src/entities/data.department.entity.ts new file mode 100644 index 0000000..0a1d70e --- /dev/null +++ b/src/entities/data.department.entity.ts @@ -0,0 +1,11 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity({ name: 'data_department' }) +export class DataDepartment { + @PrimaryColumn({ generated: 'increment' }) + id: number; + @Column('varchar', { unique: true, length: 20 }) + name: string; + @Column('timestamp') + craeteTime: string; +} diff --git a/src/entities/data.permission.entity.ts b/src/entities/data.permission.entity.ts new file mode 100644 index 0000000..27a923a --- /dev/null +++ b/src/entities/data.permission.entity.ts @@ -0,0 +1,11 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity({ name: 'data_resource' }) +export class DataResource { + @PrimaryColumn({ generated: 'increment' }) + id: number; + @Column('varchar', { unique: true, length: 50 }) + name: string; + @Column('timestamp') + createTime: string; +} diff --git a/src/entities/index.ts b/src/entities/index.ts new file mode 100644 index 0000000..b9349ff --- /dev/null +++ b/src/entities/index.ts @@ -0,0 +1,4 @@ +export * from './data.department.entity'; +export * from './data.permission.entity'; +export * from './timesheet.enetity'; +export * from './user.department.entity'; diff --git a/src/entities/user.department.entity.ts b/src/entities/user.department.entity.ts new file mode 100644 index 0000000..1057e37 --- /dev/null +++ b/src/entities/user.department.entity.ts @@ -0,0 +1,13 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity({ name: 'user_department' }) +export class UserDepartment { + @PrimaryColumn({ generated: 'increment' }) + id: number; + @Column('varchar') + userid: string; + @Column('jsonb', { array: false, default: () => "'[]'", nullable: true }) + departmentids: string[]; + @Column('timestamp') + createTime: string; +} diff --git a/src/interfaces/dingTalk/user.ts b/src/interfaces/dingTalk/user.ts index e4de05f..56178fa 100644 --- a/src/interfaces/dingTalk/user.ts +++ b/src/interfaces/dingTalk/user.ts @@ -16,6 +16,14 @@ export interface IUser { dept_id_list?: string[]; phone?: string; hired_date: number; + avatar?: string; + mobile?: string; + email?: string; + work_place?: string; + remark?: string; + boss?: boolean; + role_list?: IDingTalkUserRole[]; + title?: string; } export type IDingTalkUserResult = IDingTalkBaseResult; @@ -27,6 +35,14 @@ export interface IDingTalkUser { dept_id_list: string[]; // 入职时间戳 hired_date: number; + avatar: string; + mobile: string; + email: string; + work_place: string; + remark: string; + boss: boolean; + role_list: IDingTalkUserRole[]; + title: string; } export type IDingTalkUserListIdResult = @@ -46,3 +62,9 @@ export interface IUserToken { name: string; expires_at: number; } + +export interface IDingTalkUserRole { + id: number; + name: string; + group_name: string; +} diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 75c97ba..8e276ce 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -1,25 +1,53 @@ -export interface IUserTokenInfo { - access_token: string; - sub: string; - realm_access: { - roles: string[]; - }; - preferred_username: string; - name: string; +export interface IUserResultInfo { + refresh_token: string; expires_at: number; + access_token: string; + refresh_expires_in: string; + username: string; + email: string; + phone: string; + avatar: string; + title: string; + hiredDate: string; + resources: string[]; } export class IUserInfo { - dingTalkUserId: string; userId: string; - name: string; + dingUserId: string; username: string; - roles: string[]; - token: string; - expires: number; - refreshToken: string; + departmentIds: number[]; idToken: string; - refreshExpires: number; - accessToken: string; - hiredDate: number; + resources: string[]; +} + +export interface IKeyCloakUserInfo { + exp: number; + iat: number; + auth_time: number; + jti: string; + iss: string; + aud: string[]; + sub: string; + typ: string; + azp: string; + session_state: string; + acr: string; + realm_access: IRealmaccess; + resource_access: any; + scope: string; + sid: string; + dingUserId: string; + email_verified: boolean; + name: string; + preferred_username: string; + given_name: string; + locale: string; + family_name: string; + email: string; + resourceIds: string[]; +} + +interface IRealmaccess { + roles: string[]; } diff --git a/src/main.ts b/src/main.ts index 9196cce..cf344fb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,21 @@ -import config from '@config/config'; -import { NestFactory } from '@nestjs/core'; -import KcClient from '@utils/kcClient'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const app = await NestFactory.create(AppModule); - app.setGlobalPrefix('v1'); - app.enableCors(); - await KcClient.init(); - await app.listen(config.server.port); -} -bootstrap(); +import config from '@config/config'; +import { NestFactory } from '@nestjs/core'; +import KcClient from '@utils/kcClient'; +import NodeKeycloak from 'node-keycloak'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.setGlobalPrefix('v1'); + app.enableCors(); + await NodeKeycloak.configure({ + issuer: config.keycloak.issuer, + client_id: config.keycloak.clientId, + client_secret: config.keycloak.clientSecret, + login_redirect_uri: config.keycloak.logoutRedirectUri, + logout_redirect_uri: config.keycloak.logoutRedirectUri, + }); + await KcClient.auth(); + await app.listen(config.server.port); +} +bootstrap(); diff --git a/src/modules/options.ts b/src/modules/options.ts index 7de2c06..f04c92e 100644 --- a/src/modules/options.ts +++ b/src/modules/options.ts @@ -1,4 +1,5 @@ import config from '@config/config'; +import { join } from 'path'; const { password, host, port } = config.redis; export const redisModuleOptions = { @@ -12,3 +13,11 @@ export const jwtModuleOptions = { secret: secret, signOptions: { expiresIn: expiresIn }, }; + +export const typeOrmOptions = { + type: 'postgres' as any, + ...config.postgresql, + entities: [join(__dirname, '**', '*.entity.{ts,js}')], + migrationsTableName: 'migration', + migrations: ['src/migration/*.ts'], +}; diff --git a/src/schedules/index.ts b/src/schedules/index.ts index 63f10d2..1f7b5ba 100644 --- a/src/schedules/index.ts +++ b/src/schedules/index.ts @@ -1,2 +1,3 @@ export * from './dingTalk.schedule'; export * from './timesheet.schedule'; +export * from './keycloak.schedule'; diff --git a/src/schedules/keycloak.schedule.ts b/src/schedules/keycloak.schedule.ts new file mode 100644 index 0000000..6814403 --- /dev/null +++ b/src/schedules/keycloak.schedule.ts @@ -0,0 +1,14 @@ +import config from '@config/config'; +import { Injectable } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import KcClient from '@utils/kcClient'; + +@Injectable() +export class KeyCloakSchedule { + @Cron(config.job.keyCloakAuthRule, { name: 'KeyCloakAuthSchedule' }) + async run() { + console.log('KeyCloak authenticating...'); + await KcClient.auth(); + console.log('KeyCloak auth successful.'); + } +} diff --git a/src/services/attendance.service.ts b/src/services/attendance.service.ts index c5918a2..b05a0ae 100644 --- a/src/services/attendance.service.ts +++ b/src/services/attendance.service.ts @@ -238,6 +238,8 @@ export class AttendanceService { return; } + let onDutyTime = 0; + let offDutyTime = 0; for (const data of attendance.attendance_result_list) { if (data.check_type === AttendanceCheckType.OnDuty) { subTime = moment(data.plan_check_time).diff( @@ -247,6 +249,7 @@ export class AttendanceService { if (data.time_result === TimeResultType.Late && subTime > 60) { subTime += (await this.getLeaveTimeByMinutes()) + 90; } + onDutyTime = data.user_check_time; } else if (data.check_type === AttendanceCheckType.OffDuty) { // 获取用户上班信息 const userOnDutyInfo = attendance.attendance_result_list.find( @@ -269,6 +272,7 @@ export class AttendanceService { if (data.time_result === TimeResultType.Early) { subTime += (await this.getLeaveTimeByMinutes()) + 90; } + offDutyTime = data.user_check_time; } if (subTime < 0) { _attendance.state = AttendanceState.L; @@ -279,5 +283,12 @@ export class AttendanceService { state: _attendance.state, value: _attendance.state === AttendanceState.O ? null : _attendance.value, }); + + this.attendances.push({ + state: AttendanceState.A, + value: `${onDutyTime && moment(onDutyTime).format('HH:mm:ss')} - ${ + offDutyTime && moment(offDutyTime).format('HH:mm:ss') + }`, + }); } } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 0a047f3..3cded04 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,47 +1,53 @@ -import config from '@config/config'; -import { IUserInfo, IUserTokenInfo } from '@interfaces/user'; +import { UserDepartment } from '@entities/user.department.entity'; +import { IKeyCloakUserInfo, IUserResultInfo } from '@interfaces/user'; import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import KcClient from '@utils/kcClient'; +import { InjectRepository } from '@nestjs/typeorm'; import * as jwt from 'jsonwebtoken'; -import { UserService } from './user.service'; +import NodeKeycloak from 'node-keycloak'; +import { Repository } from 'typeorm'; @Injectable() export class AuthService { constructor( private readonly jwtService: JwtService, - private readonly userService: UserService, + @InjectRepository(UserDepartment) + private readonly userDepartmentRepository: Repository, ) {} - async signin(code: string, state: string, session_state: string) { + async signin(code: string, session_state: string): Promise { try { - const result = await KcClient.client.callback( - config.keycloak.redirectUri, - { code: code, state: state, session_state: session_state }, - ); - const data = jwt.decode(result.access_token); - const dingTalkUserInfo = await this.userService.getDingTalkUserInfoByName( - data.preferred_username, + const result = await NodeKeycloak.callback({ + code: code, + session_state: session_state, + }); + const userinfo = await NodeKeycloak.userinfo(result.access_token); + const keyCloakUserInfo = ( + jwt.decode(result.access_token) ); + const userDepartement = await this.userDepartmentRepository.findOneBy({ + userid: userinfo.sub, + }); - const userToken = { - name: data.name, - username: data.preferred_username, - roles: data.realm_access.roles, - accessToken: result.access_token, - hiredDate: dingTalkUserInfo?.hired_date, - idToken: result.id_token, - }; return { - ...userToken, - refreshToken: result.refresh_token, - refreshExpires: result.refresh_expires_in, - expires: result.expires_at, - token: this.jwtService.sign({ - ...userToken, - userId: data.sub, - dingTalkUserId: dingTalkUserInfo?.id, + expires_at: result.expires_at, + access_token: this.jwtService.sign({ + userId: keyCloakUserInfo.sub, + dingUserId: keyCloakUserInfo.dingUserId, + username: keyCloakUserInfo.preferred_username, + departmentIds: userDepartement.departmentids, + idToken: result.id_token, + resources: keyCloakUserInfo.resourceIds, }), + refresh_expires_in: result.refresh_expires_in as string, + refresh_token: result.refresh_token, + username: userinfo.preferred_username, + email: userinfo.email, + phone: userinfo.phoneNumber as string, + avatar: userinfo.avatar as string, + title: userinfo.title as string, + hiredDate: userinfo.hiredDate as string, + resources: keyCloakUserInfo.resourceIds, }; } catch (e) { console.log('Login error: ', e); @@ -49,9 +55,6 @@ export class AuthService { } async signout(token: string) { - return await KcClient.client.endSessionUrl({ - id_token_hint: token, - post_logout_redirect_uri: config.keycloak.logoutRedirectUri, - }); + return await await NodeKeycloak.signout(token); } } diff --git a/src/services/department.service.ts b/src/services/department.service.ts new file mode 100644 index 0000000..1a9d2aa --- /dev/null +++ b/src/services/department.service.ts @@ -0,0 +1,26 @@ +import { UserDepartment } from '@entities/user.department.entity'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import KcClient from '@utils/kcClient'; +import { Repository } from 'typeorm'; + +@Injectable() +export class UserDepartmentService { + constructor( + @InjectRepository(UserDepartment) + private readonly userDepartmentRepository: Repository, + ) {} + + async getDepartmentMembers(departmentids: number[]) { + const query = await this.userDepartmentRepository.createQueryBuilder(); + // Eg: + // SELECT * FROM "user_department" WHERE (departmentids)::jsonb ? '77759326-cd81-43b3-b31f-a76495c7f1ba'; + // SELECT * FROM "user_department" WHERE departmentids @> '["77759326-cd81-43b3-b31f-a76495c7f1ba","5104abac-1406-4357-bb82-d9066b689f4d"]'; + const data = await query + .where(`departmentids @> '${JSON.stringify(departmentids)}'`) + .getMany(); + const userids = data.map((x) => x.userid); + const users = await KcClient.kcAdminClient.users.find(); + return users.filter((x) => userids.includes(x.id)); + } +} diff --git a/src/services/dingTalk.service.ts b/src/services/dingTalk.service.ts index b678179..ffa7ec3 100644 --- a/src/services/dingTalk.service.ts +++ b/src/services/dingTalk.service.ts @@ -56,6 +56,14 @@ export class DingTalkService { dept_name: '', phone: '', hired_date: user['hired_date'], + avatar: user['avatar'], + mobile: user['mobile'], + email: user['email'], + work_place: user['work_place'], + remark: user['remark'], + boss: user['boss'], + role_list: user['role_list'], + title: user['title'], }); } return users; diff --git a/src/services/index.ts b/src/services/index.ts index 9c68637..c1b5b92 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -4,3 +4,5 @@ export * from './report.service'; export * from './auth.service'; export * from './user.service'; export * from './inform.service'; +export * from './timesheet.service'; +export * from './department.service'; diff --git a/src/services/permission.service.ts b/src/services/permission.service.ts new file mode 100644 index 0000000..1f7539c --- /dev/null +++ b/src/services/permission.service.ts @@ -0,0 +1,21 @@ +// import { UserDepartment } from '@entities/user.department.entity'; +// import { Injectable } from '@nestjs/common'; +// import { InjectRepository } from '@nestjs/typeorm'; +// import { Repository } from 'typeorm'; + +// @Injectable() +// export class PermissionService { +// constructor( +// @InjectRepository(DepartmentPermission) +// private readonly departmentRepository: Repository, +// ) {} + +// async getDepartmentMembers(departmentids: string[]) { +// const query = await this.departmentRepository.createQueryBuilder(); +// const data = await query +// .where(`departmentid in ${departmentids.join(',')}`) +// .getMany(); +// const permissions = data.map((x) => x.permissions.join(',')); +// return permissions; +// } +// } diff --git a/src/services/timesheet.service.ts b/src/services/timesheet.service.ts new file mode 100644 index 0000000..742bea9 --- /dev/null +++ b/src/services/timesheet.service.ts @@ -0,0 +1,19 @@ +import { UserTimesheet } from '@entities/timesheet.enetity'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +@Injectable() +export class TimeSheetService { + constructor( + @InjectRepository(UserTimesheet) + private readonly timesheetRepository: Repository, + ) {} + + async getTimeSheetByDate(createTime: string) { + const data = await this.timesheetRepository.findOneBy({ + createTime: createTime, + }); + return data?.timesheet || []; + } +} diff --git a/src/services/user.service.ts b/src/services/user.service.ts index f1f1a15..84145b9 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -5,10 +5,20 @@ import { ITimeSheet } from '@interfaces/timesheet'; import { Injectable } from '@nestjs/common'; import * as moment from 'moment'; import { InjectRedis, Redis } from '@nestjs-modules/ioredis'; +import config from '@config/config'; +import { UserDepartment } from '@entities/user.department.entity'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { intersect, now } from '@utils/utils'; +import KcClient from '@utils/kcClient'; @Injectable() export class UserService { - constructor(@InjectRedis() private readonly redis: Redis) {} + constructor( + @InjectRedis() private readonly redis: Redis, + @InjectRepository(UserDepartment) + private readonly departmentRepository: Repository, + ) {} async getTodayTimeSheet(username: string) { const data = await this.redis.get('timesheets'); @@ -61,4 +71,82 @@ export class UserService { const users = await FileData.readUsers(); return users.find((x) => x.name === username); } + + async generateUserDepartment() { + const users = await KcClient.kcAdminClient.users.find(); + let departmentids = []; + for (const user of users) { + if (['陶智', '胡秋成'].includes(user.username)) { + departmentids = [3]; + } else if (['王中伟'].includes(user.username)) { + departmentids = [5]; + } else if (['王祯鹏'].includes(user.username)) { + departmentids = [4]; + } else if (['王志峰'].includes(user.username)) { + departmentids = [1, 2, 3, 4, 5]; + } else if ( + ['代瓒', '谈娜', '吴美美', '张华尘', '何三星'].includes(user.username) + ) { + departmentids = [2]; + } else { + departmentids = [1]; + } + await this.departmentRepository.save({ + userid: user.id, + departmentids: departmentids, + createTime: now(), + }); + + user.attributes = { + ...user.attributes, + departmentids: departmentids.join(','), + }; + + await KcClient.kcAdminClient.users.update({ id: user.id }, user); + } + } + + async updateUserDepartment() { + const users = await KcClient.kcAdminClient.users.find(); + let departmentids = []; + for (const user of users) { + departmentids = [1, 5]; + // if (['陶智', '胡秋成'].includes(user.username)) { + // departmentids = [2]; + // } else if (['王中伟'].includes(user.username)) { + // departmentids = [5]; + // } else if (['王祯鹏'].includes(user.username)) { + // departmentids = [4]; + // } else if (['王志峰'].includes(user.username)) { + // departmentids = [1, 2, 3, 4, 5]; + // } else if ( + // ['代瓒', '谈娜', '吴美美', '张华尘', '何三星'].includes(user.username) + // ) { + // departmentids = [1]; + // } + // this.departmentRepository.create({ + // userid: user.id, + // departmentids: departmentids, + // createTime: now(), + // }); + } + } + + async getUsers(departmentids?: string[]) { + let users = await KcClient.kcAdminClient.users.find(); + if (departmentids) { + users = users.filter( + (x) => + intersect(departmentids, x.attributes['departmentids']).length > 0, + ); + } + return users; + } + + async updateUserResource(userid: string, resourceIds: string) { + const user = await KcClient.kcAdminClient.users.findOne({ id: userid }); + user.attributes['resourceIds'] = resourceIds; + await KcClient.kcAdminClient.users.update({ id: userid }, user); + return user; + } } diff --git a/src/sockets/timesheet.socket.ts b/src/sockets/timesheet.socket.ts index 9aa3dfc..55b50e1 100644 --- a/src/sockets/timesheet.socket.ts +++ b/src/sockets/timesheet.socket.ts @@ -19,7 +19,7 @@ export class TimeSheetSocket { data.updateTime = now(); if (_data) { // 使用token中的userid=>每用户都只能编辑自己的timesheet不能修改其他用户的 - // _data.userid = client.data.dingTalkUserId; + // _data.userid = client.data.dingUserId; _data.userid = data.userid; _data.value = data.value; _data.createTime = data.createTime || now(); diff --git a/src/strategys/jwt.strategy.ts b/src/strategys/jwt.strategy.ts index ce05844..6de97b4 100644 --- a/src/strategys/jwt.strategy.ts +++ b/src/strategys/jwt.strategy.ts @@ -16,11 +16,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) { async validate(payload: IUserInfo) { return { - dingTalkUserId: payload.dingTalkUserId, + dingUserId: payload.dingUserId, userId: payload.userId, username: payload.username, - roles: payload.roles, + departmentIds: payload.departmentIds, idToken: payload.idToken, + resources: payload.resources, }; } } diff --git a/src/utils/kcClient.ts b/src/utils/kcClient.ts index 8462a2c..186af6a 100644 --- a/src/utils/kcClient.ts +++ b/src/utils/kcClient.ts @@ -1,15 +1,18 @@ import config from '@config/config'; -import { Issuer, BaseClient } from 'openid-client'; +import KcAdminClient from '@keycloak/keycloak-admin-client'; export default class KcClient { - static client: BaseClient; - static async init() { - const { clientId, clientSecret, redirectUri, issuer } = config.keycloak; - const keycloakIssuer = await Issuer.discover(issuer); - this.client = new keycloakIssuer.Client({ - client_id: clientId, - client_secret: clientSecret, - redirect_uris: [redirectUri], - response_types: ['code'], + static kcAdminClient = new KcAdminClient(); + static async auth() { + this.kcAdminClient.setConfig({ + baseUrl: config.keycloak.clientBaseUrl, + realmName: config.keycloak.realm, + }); + await this.kcAdminClient.auth({ + username: config.keycloak.username, + password: config.keycloak.password, + grantType: config.keycloak.clientGrantType, + clientId: config.keycloak.clientId, + clientSecret: config.keycloak.clientSecret, }); } } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 9f03679..b90a3be 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -7,6 +7,12 @@ import { AttendanceState } from '../constants/dingTalk'; export function unique(arr) { return Array.from(new Set(arr)); } +/** + * 简单数据取交集 + */ +export function intersect(arr1: any[], arr2: any[]) { + return arr1.filter(Set.prototype.has, new Set(arr2)); +} export function vacationToEnum(name) { switch (name) { diff --git a/tsconfig.json b/tsconfig.json index 2f7f145..d42b47c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,33 +1,56 @@ -{ - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "es2017", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false, - "paths": { - "@core/*": ["./src/core/*"], - "@controller/*": ["./src/controller/*"], - "@config/*": ["./src/config/*"], - "@services/*": ["./src/services/*"], - "@apis/*": ["./src/apis/*"], - "@utils/*": ["./src/utils/*"], - "@interfaces/*": ["./src/interfaces/*"], - "@constants/*": ["./src/constants/*"], - "@dtos/*": ["./src/dtos/*"], - "@strategys/*": ["./src/strategys/*"] - } - } -} +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "es2017", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "paths": { + "@core/*": [ + "./src/core/*" + ], + "@controller/*": [ + "./src/controller/*" + ], + "@config/*": [ + "./src/config/*" + ], + "@services/*": [ + "./src/services/*" + ], + "@apis/*": [ + "./src/apis/*" + ], + "@utils/*": [ + "./src/utils/*" + ], + "@interfaces/*": [ + "./src/interfaces/*" + ], + "@constants/*": [ + "./src/constants/*" + ], + "@dtos/*": [ + "./src/dtos/*" + ], + "@strategys/*": [ + "./src/strategys/*" + ], + "@entities/*": [ + "./src/entities/*" + ] + } + } +} diff --git a/yarn.lock b/yarn.lock index 8ef19dc..cdde92f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -632,6 +632,19 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@keycloak/keycloak-admin-client@^18.0.2": + version "18.0.2" + resolved "https://registry.yarnpkg.com/@keycloak/keycloak-admin-client/-/keycloak-admin-client-18.0.2.tgz#e8329830ea2bc9fc7012e31b10c06a35ab58984c" + integrity sha512-UCa+5FTPBzbbfCpC27Sb40XbNm27m78z+yax9kiw9aFwk+itiGId09bMzECBRDrqwvVMxo1vzLERLjAty3rTRg== + dependencies: + axios "^0.26.1" + camelize-ts "^1.0.8" + keycloak-js "^17.0.1" + lodash "^4.17.21" + query-string "^7.0.1" + url-join "^4.0.0" + url-template "^2.0.8" + "@nestjs-modules/ioredis@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@nestjs-modules/ioredis/-/ioredis-1.0.0.tgz" @@ -1511,6 +1524,13 @@ axios@0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" +axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + babel-jest@^28.1.0: version "28.1.0" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.0.tgz" @@ -1576,7 +1596,7 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: +base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -1757,6 +1777,11 @@ camelcase@^6.2.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +camelize-ts@^1.0.8: + version "1.0.9" + resolved "https://registry.yarnpkg.com/camelize-ts/-/camelize-ts-1.0.9.tgz#6ac46fbe660d18e093568ef0d56c836141b700f4" + integrity sha512-ePOW3V2qrQ0qtRlcTM6Qe3nXremdydIwsMKI1Vl2NBGM0tOo8n2xzJ7YOQpV1GIKHhs3p+F40ThI8/DoYWbYKQ== + caniuse-lite@^1.0.30001332: version "1.0.30001342" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001342.tgz" @@ -2111,6 +2136,11 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3 dependencies: ms "2.1.2" +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== + decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz" @@ -2649,6 +2679,11 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== + finalhandler@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" @@ -2683,6 +2718,11 @@ flatted@^3.1.0: resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== +follow-redirects@^1.14.8: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + follow-redirects@^1.14.9: version "1.15.0" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz" @@ -3665,6 +3705,11 @@ jose@^4.1.4: resolved "https://registry.npmjs.org/jose/-/jose-4.8.1.tgz" integrity sha512-+/hpTbRcCw9YC0TOfN1W47pej4a9lRmltdOVdRLz5FP5UvUq3CenhXjQK7u/8NdMIIShMXYAh9VLPhc7TjhvFw== +js-sha256@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" + integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -3781,6 +3826,14 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +keycloak-js@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-17.0.1.tgz#403ea75b3e938ddc780f99ecbd73e1b6905f826f" + integrity sha512-mbLBSoogCBX5VYeKCdEz8BaRWVL9twzSqArRU3Mo3Z7vEO1mghGZJ5IzREfiMEi7kTUZtk5i9mu+Yc0koGkK6g== + dependencies: + base64-js "^1.5.1" + js-sha256 "^0.9.0" + keyv@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz" @@ -4141,6 +4194,13 @@ node-int64@^0.4.0: resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= +node-keycloak@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/node-keycloak/-/node-keycloak-0.1.5.tgz#52ad3cdab9ea8605c6cae5dc3b86ca8722339619" + integrity sha512-wD2pqbs/yboHMC/HAp8dfGVahG8UpgSOVgqdtw4SBKqod3bHRqXJ47O+5R1D+G2mnEcQTQBesfiha+jU3PGe+A== + dependencies: + openid-client "^5.1.6" + node-releases@^2.0.3: version "2.0.5" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz" @@ -4625,6 +4685,16 @@ qs@6.9.3: resolved "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz" integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== +query-string@^7.0.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.1.tgz#754620669db978625a90f635f12617c271a088e1" + integrity sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w== + dependencies: + decode-uri-component "^0.2.0" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -5071,6 +5141,11 @@ sourcemap-codec@^1.4.4: resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + split2@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" @@ -5103,6 +5178,11 @@ streamsearch@0.1.2: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz" integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -5577,6 +5657,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-join@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + url-parse-lax@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz" @@ -5584,6 +5669,11 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +url-template@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" + integrity sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw== + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"