feat: 添加飞书以及github登录

This commit is contained in:
lichao 2025-01-15 15:55:59 +08:00
parent 946be43558
commit 219afa0353
14 changed files with 234 additions and 58 deletions

10
.env
View File

@ -3,4 +3,12 @@ SERVER_HOST='192.144.32.178'
SERVER_PORT=3306
SERVER_USER='root'
PASSWORD='lichao1314'
DB_NAME='auth_db'
DB_NAME='auth_db'
GITHUB_CLIENT_ID=Ov23lihk723FlNAwlFg6
GITHUB_CLIENT_SECRET=b839f50bba1f006ffdd43fb73c5ae221a54e1e2e
GITHUB_CALLBACK_URL=http://localhost:3030/auth/github/callback
FEISHU_APP_ID=cli_a66a897f687a5013
FEISHU_APP_SECRET=s106GAbbCZk66OcHN69Rng5TaLK6fiH2

View File

@ -29,12 +29,16 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^10.0.2",
"@types/bcrypt": "^5.0.2",
"@types/passport-github2": "^1.2.9",
"@types/uuid": "^10.0.0",
"axios": "^1.7.9",
"bcrypt": "^5.1.1",
"bcryptjs": "^2.4.3",
"dotenv": "^16.4.7",
"install": "^0.13.0",
"mysql2": "^3.12.0",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",

View File

@ -16,13 +16,16 @@ specifiers:
'@types/express': ^4.17.17
'@types/jest': ^29.5.2
'@types/node': ^20.3.1
'@types/passport-github2': ^1.2.9
'@types/passport-jwt': ^4.0.1
'@types/supertest': ^6.0.0
'@types/uuid': ^10.0.0
'@typescript-eslint/eslint-plugin': ^6.0.0
'@typescript-eslint/parser': ^6.0.0
axios: ^1.7.9
bcrypt: ^5.1.1
bcryptjs: ^2.4.3
dotenv: ^16.4.7
eslint: ^8.42.0
eslint-config-prettier: ^9.0.0
eslint-plugin-prettier: ^5.0.0
@ -30,6 +33,7 @@ specifiers:
jest: ^29.5.0
mysql2: ^3.12.0
passport: ^0.7.0
passport-github2: ^0.1.12
passport-jwt: ^4.0.1
passport-local: ^1.0.0
prettier: ^3.0.0
@ -55,12 +59,16 @@ dependencies:
'@nestjs/platform-express': 10.4.15_5u4hn6whjn5aawl2edmluzh4i4
'@nestjs/typeorm': 10.0.2_5ay6scu5luhdsc23ie3iqrw3sm
'@types/bcrypt': 5.0.2
'@types/passport-github2': 1.2.9
'@types/uuid': 10.0.0
axios: 1.7.9
bcrypt: 5.1.1
bcryptjs: 2.4.3
dotenv: 16.4.7
install: 0.13.0
mysql2: 3.12.0
passport: 0.7.0
passport-github2: 0.1.12
passport-jwt: 4.0.1
passport-local: 1.0.0
reflect-metadata: 0.1.14
@ -1210,13 +1218,11 @@ packages:
dependencies:
'@types/connect': 3.4.38
'@types/node': 20.17.12
dev: true
/@types/connect/3.4.38:
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
dependencies:
'@types/node': 20.17.12
dev: true
/@types/cookiejar/2.1.5:
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
@ -1247,7 +1253,6 @@ packages:
'@types/qs': 6.9.17
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
dev: true
/@types/express/4.17.21:
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
@ -1256,7 +1261,6 @@ packages:
'@types/express-serve-static-core': 4.19.6
'@types/qs': 6.9.17
'@types/serve-static': 1.15.7
dev: true
/@types/graceful-fs/4.1.9:
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
@ -1266,7 +1270,6 @@ packages:
/@types/http-errors/2.0.4:
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
dev: true
/@types/istanbul-lib-coverage/2.0.6:
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
@ -1306,13 +1309,26 @@ packages:
/@types/mime/1.3.5:
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
dev: true
/@types/node/20.17.12:
resolution: {integrity: sha512-vo/wmBgMIiEA23A/knMfn/cf37VnuF52nZh5ZoW0GWt4e4sxNquibrMRJ7UQsA06+MBx9r/H1jsI9grYjQCQlw==}
dependencies:
undici-types: 6.19.8
/@types/oauth/0.9.6:
resolution: {integrity: sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==}
dependencies:
'@types/node': 20.17.12
dev: false
/@types/passport-github2/1.2.9:
resolution: {integrity: sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA==}
dependencies:
'@types/express': 4.17.21
'@types/passport': 1.0.17
'@types/passport-oauth2': 1.4.17
dev: false
/@types/passport-jwt/4.0.1:
resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==}
dependencies:
@ -1320,6 +1336,14 @@ packages:
'@types/passport-strategy': 0.2.38
dev: true
/@types/passport-oauth2/1.4.17:
resolution: {integrity: sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==}
dependencies:
'@types/express': 4.17.21
'@types/oauth': 0.9.6
'@types/passport': 1.0.17
dev: false
/@types/passport-strategy/0.2.38:
resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==}
dependencies:
@ -1331,15 +1355,12 @@ packages:
resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==}
dependencies:
'@types/express': 4.17.21
dev: true
/@types/qs/6.9.17:
resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==}
dev: true
/@types/range-parser/1.2.7:
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
dev: true
/@types/semver/7.5.8:
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
@ -1350,7 +1371,6 @@ packages:
dependencies:
'@types/mime': 1.3.5
'@types/node': 20.17.12
dev: true
/@types/serve-static/1.15.7:
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
@ -1358,7 +1378,6 @@ packages:
'@types/http-errors': 2.0.4
'@types/node': 20.17.12
'@types/send': 0.17.4
dev: true
/@types/stack-utils/2.0.3:
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
@ -1839,13 +1858,22 @@ packages:
/asynckit/0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: true
/aws-ssl-profiles/1.1.2:
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
engines: {node: '>= 6.0.0'}
dev: false
/axios/1.7.9:
resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==}
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.1
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
dev: false
/babel-jest/29.7.0_@babel+core@7.26.0:
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -1927,6 +1955,11 @@ packages:
/base64-js/1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
/base64url/3.0.1:
resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==}
engines: {node: '>=6.0.0'}
dev: false
/bcrypt/5.1.1:
resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==}
engines: {node: '>= 10.0.0'}
@ -2239,7 +2272,6 @@ packages:
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
dev: true
/commander/2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@ -2426,7 +2458,6 @@ packages:
/delayed-stream/1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dev: true
/delegates/1.0.0:
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
@ -2951,6 +2982,16 @@ packages:
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
dev: true
/follow-redirects/1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: false
/foreground-child/3.3.0:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
@ -2988,7 +3029,6 @@ packages:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: true
/formidable/2.1.2:
resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==}
@ -4438,6 +4478,10 @@ packages:
set-blocking: 2.0.0
dev: false
/oauth/0.10.0:
resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==}
dev: false
/object-assign/4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@ -4567,6 +4611,13 @@ packages:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
/passport-github2/0.1.12:
resolution: {integrity: sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==}
engines: {node: '>= 0.8.0'}
dependencies:
passport-oauth2: 1.8.0
dev: false
/passport-jwt/4.0.1:
resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==}
dependencies:
@ -4581,6 +4632,17 @@ packages:
passport-strategy: 1.0.0
dev: false
/passport-oauth2/1.8.0:
resolution: {integrity: sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==}
engines: {node: '>= 0.4.0'}
dependencies:
base64url: 3.0.1
oauth: 0.10.0
passport-strategy: 1.0.0
uid2: 0.0.4
utils-merge: 1.0.1
dev: false
/passport-strategy/1.0.0:
resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==}
engines: {node: '>= 0.4.0'}
@ -4710,6 +4772,10 @@ packages:
forwarded: 0.2.0
ipaddr.js: 1.9.1
/proxy-from-env/1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
dev: false
/punycode/2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@ -5567,6 +5633,10 @@ packages:
dependencies:
'@lukeed/csprng': 1.1.0
/uid2/0.0.4:
resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==}
dev: false
/undici-types/6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}

View File

@ -1,22 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@ -5,6 +5,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { User } from './user/entities/user.entity';
import { AuthModule } from './auth/auth.module';
import { LarkModule } from './lark/lark.module';
@Module({
imports: [
@ -25,6 +27,8 @@ import { User } from './user/entities/user.entity';
}),
TypeOrmModule.forFeature([User]),
UserModule,
AuthModule,
LarkModule,
],
controllers: [AppController],
providers: [AppService],

View File

@ -0,0 +1,23 @@
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
@Controller('auth')
export class AuthController {
@Get('github')
@UseGuards(AuthGuard('github'))
async githubLogin() {
// GitHub 登录重定向
}
@Get('github/callback')
@UseGuards(AuthGuard('github'))
async githubCallback(@Req() req, @Res() res: Response) {
const user = req.user;
// 构造前端 URL附带用户信息或 Token
const frontendUrl = `http://localhost:8000/?username=${user.username}&email=${user.email}`;
// 重定向到前端
return res.redirect(frontendUrl);
}
}

11
src/auth/auth.module.ts Normal file
View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
import { GitHubStrategy } from './github.strategy';
@Module({
imports: [PassportModule],
controllers: [AuthController],
providers: [GitHubStrategy],
})
export class AuthModule {}

4
src/auth/auth.service.ts Normal file
View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {}

View File

@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-github2';
@Injectable()
export class GitHubStrategy extends PassportStrategy(Strategy, 'github') {
constructor() {
super({
clientID: process.env.GITHUB_CLIENT_ID, // 从环境变量加载
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: process.env.GITHUB_CALLBACK_URL, // 回调 URL
scope: ['user:email'], // 需要的权限
});
}
async validate(accessToken: string, refreshToken: string, profile: any) {
// 返回用户信息,或者直接处理用户登录逻辑
const { id, username, emails } = profile;
return {
id,
username,
email: emails?.[0]?.value || null,
};
}
}

View File

@ -0,0 +1,30 @@
import { Controller, Get, Query, Res } from '@nestjs/common';
import { LarkService } from './lark.service';
import { Response } from 'express';
@Controller('lark')
export class LarkController {
constructor(private readonly larkService: LarkService) {}
@Get('login')
async login(@Query('redirectUri') redirectUri: string, @Res() res: Response) {
const loginUrl = await this.larkService.getLoginUrl(redirectUri);
return res.redirect(loginUrl);
}
@Get('callback')
async callback(@Query('code') code: string, @Res() res: Response) {
if (!code) {
return res.status(400).send('Authorization code is missing');
}
const tokenData = await this.larkService.getAccessToken(code);
if (tokenData && tokenData.code === 0) {
const user = tokenData.data;
return res.json({ message: 'Login successful', user });
} else {
return res.status(400).send('Failed to retrieve access token');
}
}
}

9
src/lark/lark.module.ts Normal file
View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { LarkService } from './lark.service';
import { LarkController } from './lark.controller';
@Module({
controllers: [LarkController],
providers: [LarkService],
})
export class LarkModule {}

27
src/lark/lark.service.ts Normal file
View File

@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import axios from 'axios';
@Injectable()
export class LarkService {
private readonly appId = process.env.FEISHU_APP_ID;
private readonly appSecret = process.env.FEISHU_APP_SECRET;
async getLoginUrl(redirectUri: string): Promise<string> {
return `https://open.feishu.cn/open-apis/authen/v1/index?app_id=${this.appId}&redirect_uri=${encodeURIComponent(
redirectUri,
)}`;
}
async getAccessToken(authCode: string): Promise<any> {
const url = 'https://open.feishu.cn/open-apis/authen/v1/access_token';
const data = {
grant_type: 'authorization_code',
code: authCode,
app_id: this.appId,
app_secret: this.appSecret,
};
const response = await axios.post(url, data);
return response.data;
}
}

View File

@ -2,6 +2,9 @@ import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as mysql from 'mysql2/promise';
import { ConfigService } from '@nestjs/config';
import * as dotenv from 'dotenv';
dotenv.config();
async function bootstrap() {
// 启动 Nest 应用

View File

@ -1,20 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from './user.service';
describe('UserController', () => {
let controller: UserController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [UserService],
}).compile();
controller = module.get<UserController>(UserController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});