diff --git a/docker-compose.yml b/docker-compose.yml index 5a0f91a..3648f14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,27 +46,42 @@ services: volumes: - pgadmin:/root/.pgadmin -# chat-service: -# container_name: chat-service -# build: ./src/chat-service -# env_file: -# - ./src/chat-service/.env -# volumes: -# - ./src/chat-service:/chat -# - /chat/node_modules -# ports: -# - ${CHAT_SERVICE_PORT}:${CHAT_SERVICE_PORT} -# restart: always -# -# rmq: -# container_name: rmq -# image: rabbitmq:3-management -# env_file: -# - .env -# ports: -# - ${RABBITMQ_LOCAL_PORT}:${RABBITMQ_NATIVE_PORT} -# - "5672:5672" -# restart: always + chat-service: + container_name: chat-service + build: ./src/chat-service + env_file: + - ./src/chat-service/.env + volumes: + - ./src/chat-service:/chat + - /chat/node_modules + ports: + - ${CHAT_SERVICE_PORT}:${CHAT_SERVICE_PORT} + depends_on: + - postgres-chat + restart: always + + postgres-chat: + container_name: postgres_chat + image: postgres:15 + env_file: + - ./src/chat-service/.env + environment: + PG_DATA: /var/lib/postgresql/data + ports: + - ${CHAT_POSTGRES_PORT}:${POSTGRES_PORT} + volumes: + - pgdata-chat:/var/lib/postgresql/data + restart: always + + rmq: + container_name: rmq + image: rabbitmq:3-management + env_file: + - .env + ports: + - ${RABBITMQ_LOCAL_PORT}:${RABBITMQ_NATIVE_PORT} + - '5672:5672' + restart: always volumes: pgdata: diff --git a/package.json b/package.json index 19f8adb..3b95c77 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", "@nestjs/jwt": "^10.0.1", + "@nestjs/passport": "^9.0.3", "@nestjs/platform-express": "^9.0.0", "@nestjs/platform-socket.io": "^9.2.1", "@nestjs/sequelize": "^9.0.0", @@ -44,6 +45,8 @@ "cookie-parser": "^1.4.6", "cross-env": "^7.0.3", "dotenv": "^16.0.3", + "passport": "^0.6.0", + "passport-google-oauth20": "^2.0.0", "pg": "^8.8.0", "pg-hstore": "^2.3.4", "reflect-metadata": "^0.1.13", @@ -63,6 +66,7 @@ "@types/express": "^4.17.13", "@types/jest": "28.1.8", "@types/node": "^16.0.0", + "@types/passport-google-oauth20": "^2.0.11", "@types/sequelize": "^4.28.14", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", diff --git a/src/app.module.ts b/src/app.module.ts index 4b5b735..4394c4e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,6 +23,11 @@ import { UserAccess } from './user/user.access.model'; import { UserModule } from './user/user.module'; import { AuthModule } from './auth/auth.module'; import { InventoryModule } from './inventory/inventory.module'; +import { FeatureModule } from './feature/feature.module'; +import { FeatureFlagModule } from './feature-flag/feature-flag.module'; +import { FeatureFlag } from './feature-flag/feature-flag.model'; +import { Feature } from './feature/feature.model'; +import { GoogleUser } from './auth/oauth2/google/google.model'; @Module({ imports: [ @@ -47,6 +52,9 @@ import { InventoryModule } from './inventory/inventory.module'; Question, Quiz_Question, UserAccess, + Feature, + FeatureFlag, + GoogleUser, ], autoLoadModels: true, }), @@ -59,6 +67,8 @@ import { InventoryModule } from './inventory/inventory.module'; ModerationModule, QuestionsModule, InventoryModule, + FeatureModule, + FeatureFlagModule, ], controllers: [AppController], diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index d31e025..c409baf 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -21,6 +21,7 @@ import { Cookies } from './decorators/cookies.decorator'; import { JwtAuthGuard } from './jwt-auth.guard'; import { UserInReq } from './decorators/users.decorator'; import { UserReqDto } from './dto/user-req.dto'; +import { GoogleAuthGuard } from './oauth2/google/google-auth.guard'; @ApiTags('Authorization') @Controller('/api/auth') @@ -124,4 +125,16 @@ export class AuthController { async checkCurrentUser(@UserInReq() user: UserReqDto) { return this.authService.checkCurrentUser(user.userId); } + + @Get('google/login') + @UseGuards(GoogleAuthGuard) + handleLogin() { + return { msg: 'Google Authentication' }; + } + + @Get('google/redirect') + @UseGuards(GoogleAuthGuard) + handleRedirect() { + return { msg: 'OK' }; + } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 8756653..d729ca5 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -5,9 +5,14 @@ import { UserModule } from '../user/user.module'; import { JwtModule } from '@nestjs/jwt'; import { AccessGroupModule } from '../access-group/access-group.module'; import { PermissionModule } from '../permission/permission.module'; +import { GoogleStrategy } from './oauth2/google/google.strategy'; +import { SessionSerializer } from './oauth2/google/serializer'; +import { SequelizeModule } from '@nestjs/sequelize'; +import { GoogleUser } from './oauth2/google/google.model'; @Module({ imports: [ + SequelizeModule.forFeature([GoogleUser]), UserModule, AccessGroupModule, PermissionModule, @@ -16,7 +21,15 @@ import { PermissionModule } from '../permission/permission.module'; }), ], controllers: [AuthController], - providers: [AuthService], + providers: [ + AuthService, + GoogleStrategy, + SessionSerializer, + { + provide: 'GOOGLE_AUTH_SERVICE', + useClass: AuthService, + }, + ], exports: [AuthService, JwtModule], }) export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 589d498..38c568a 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -10,10 +10,15 @@ import { CreateUserDto } from '../user/dto/create-user.dto'; import { LoginUserDto, UserViewType } from './dto/login-user.dto'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; +import { GoogleLoginUserDto } from './oauth2/google/dto/google-login-user.dto'; +import { InjectModel } from '@nestjs/sequelize'; +import { GoogleUser } from './oauth2/google/google.model'; @Injectable() export class AuthService { constructor( + @InjectModel(GoogleUser) + private readonly googleUserModel: typeof GoogleUser, private userService: UserService, private jwtService: JwtService, ) {} @@ -100,4 +105,19 @@ export class AuthService { }; return userInfo; } + + async validateGoogleUser(details: GoogleLoginUserDto) { + console.log('AuthService'); + console.log(details); + const googleUser = await this.googleUserModel.findOne({ + where: { email: details.email }, + }); + console.log(googleUser); + if (googleUser) return googleUser; + console.log('User not found. Creating...'); + return this.googleUserModel.create(details); + } + async findGoogleUser(id: number) { + return await this.googleUserModel.findOne({ where: { id } }); + } } diff --git a/src/auth/oauth2/google/dto/google-login-user.dto.ts b/src/auth/oauth2/google/dto/google-login-user.dto.ts new file mode 100644 index 0000000..04ac3cf --- /dev/null +++ b/src/auth/oauth2/google/dto/google-login-user.dto.ts @@ -0,0 +1,6 @@ +export class GoogleLoginUserDto { + readonly googleId: string; + readonly email: string; + readonly displayName: string; + readonly photo: string; +} diff --git a/src/auth/oauth2/google/google-auth.guard.ts b/src/auth/oauth2/google/google-auth.guard.ts new file mode 100644 index 0000000..4bda3fd --- /dev/null +++ b/src/auth/oauth2/google/google-auth.guard.ts @@ -0,0 +1,12 @@ +import { AuthGuard } from '@nestjs/passport'; +import { ExecutionContext, Injectable } from '@nestjs/common'; + +@Injectable() +export class GoogleAuthGuard extends AuthGuard('google') { + async canActive(context: ExecutionContext) { + const activate = (await super.canActivate(context)) as boolean; + const request = context.switchToHttp().getRequest(); + await super.logIn(request); + return activate; + } +} diff --git a/src/auth/oauth2/google/google.model.ts b/src/auth/oauth2/google/google.model.ts new file mode 100644 index 0000000..ac79b8c --- /dev/null +++ b/src/auth/oauth2/google/google.model.ts @@ -0,0 +1,42 @@ +import { Column, DataType, Model, Table } from 'sequelize-typescript'; + +interface GoogleUserAttrs { + googleId: string; + email: string; + displayName: string; + photo: string; +} +@Table({ tableName: 'google-user', createdAt: false, updatedAt: false }) +export class GoogleUser extends Model { + @Column({ + type: DataType.INTEGER, + unique: true, + autoIncrement: true, + primaryKey: true, + }) + id: number; + + @Column({ + type: DataType.STRING, + unique: true, + primaryKey: true, + }) + googleId: string; + + @Column({ + type: DataType.STRING, + unique: true, + }) + email: string; + + @Column({ + type: DataType.STRING, + }) + displayName: string; + + @Column({ + type: DataType.STRING, + unique: true, + }) + photo: string; +} diff --git a/src/auth/oauth2/google/google.strategy.ts b/src/auth/oauth2/google/google.strategy.ts new file mode 100644 index 0000000..61ae2dc --- /dev/null +++ b/src/auth/oauth2/google/google.strategy.ts @@ -0,0 +1,31 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Profile, Strategy } from 'passport-google-oauth20'; +import { Inject, Injectable } from '@nestjs/common'; +import * as process from 'process'; +import { AuthService } from '../../auth.service'; + +@Injectable() +export class GoogleStrategy extends PassportStrategy(Strategy) { + constructor( + @Inject('GOOGLE_AUTH_SERVICE') private readonly authService: AuthService, + ) { + super({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL, + scope: ['profile', 'email'], + }); + } + async validate(accessToken: string, refreshToken: string, profile: Profile) { + console.log(accessToken); + console.log(refreshToken); + console.log(profile); + const googleUser = await this.authService.validateGoogleUser({ + email: profile.emails[0].value, + displayName: profile.displayName, + photo: profile.photos[0].value, + googleId: profile.id, + }); + return googleUser || null; + } +} diff --git a/src/auth/oauth2/google/serializer.ts b/src/auth/oauth2/google/serializer.ts new file mode 100644 index 0000000..a656fa8 --- /dev/null +++ b/src/auth/oauth2/google/serializer.ts @@ -0,0 +1,27 @@ +import { PassportSerializer } from '@nestjs/passport'; +import { Inject, Injectable } from '@nestjs/common'; +import { AuthService } from '../../auth.service'; +import { GoogleUser } from './google.model'; + +@Injectable() +export class SessionSerializer extends PassportSerializer { + constructor( + @Inject('GOOGLE_AUTH_SERVICE') private readonly authService: AuthService, + ) { + super(); + } + + // eslint-disable-next-line @typescript-eslint/ban-types + serializeUser(user: GoogleUser, done: Function) { + console.log('Deserialize User'); + done(null, user); + } + + // eslint-disable-next-line @typescript-eslint/ban-types + async deserializeUser(payload: any, done: Function) { + const googleUser = await this.authService.findGoogleUser(payload.id); + console.log('Deserialize User'); + console.log(googleUser); + return googleUser ? done(null, googleUser) : done(null, null); + } +} diff --git a/src/feature-flag/dto/create-feature-flag.dto.ts b/src/feature-flag/dto/create-feature-flag.dto.ts new file mode 100644 index 0000000..1dc41df --- /dev/null +++ b/src/feature-flag/dto/create-feature-flag.dto.ts @@ -0,0 +1,6 @@ +export class CreateFeatureFlagDto { + readonly featureId: number; + readonly nextFeatureStatus: boolean; + readonly angularFeatureStatus: boolean; + readonly mobileFeatureStatus: boolean; +} diff --git a/src/feature-flag/feature-flag.controller.ts b/src/feature-flag/feature-flag.controller.ts new file mode 100644 index 0000000..a133b34 --- /dev/null +++ b/src/feature-flag/feature-flag.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('feature-flag') +export class FeatureFlagController {} diff --git a/src/feature-flag/feature-flag.model.ts b/src/feature-flag/feature-flag.model.ts new file mode 100644 index 0000000..20944cc --- /dev/null +++ b/src/feature-flag/feature-flag.model.ts @@ -0,0 +1,40 @@ +import { + BelongsTo, + Column, + DataType, + ForeignKey, + Model, + Table, +} from 'sequelize-typescript'; +import { Feature } from '../feature/feature.model'; + +export interface FeatureFlagCreationAttrs { + nextFeatureStatus: boolean; + angularFeatureStatus: boolean; + mobileFeatureStatus: boolean; +} + +@Table({ tableName: 'feature-flags' }) +export class FeatureFlag extends Model { + @Column({ + type: DataType.INTEGER, + unique: true, + autoIncrement: true, + primaryKey: true, + }) + id: number; + + @ForeignKey(() => Feature) + @Column({ type: DataType.INTEGER }) + featureId: number; + + @Column({ type: DataType.BOOLEAN, allowNull: false }) + nextFeatureStatus: boolean; + @Column({ type: DataType.BOOLEAN, allowNull: false }) + angularFeatureStatus: boolean; + @Column({ type: DataType.BOOLEAN, allowNull: false }) + mobileFeatureStatus: boolean; + + @BelongsTo(() => Feature) + feature: Feature; +} diff --git a/src/feature-flag/feature-flag.module.ts b/src/feature-flag/feature-flag.module.ts new file mode 100644 index 0000000..5719074 --- /dev/null +++ b/src/feature-flag/feature-flag.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { FeatureFlagService } from './feature-flag.service'; +import { FeatureFlagController } from './feature-flag.controller'; +import { SequelizeModule } from '@nestjs/sequelize'; +import { Feature } from '../feature/feature.model'; +import { FeatureFlag } from './feature-flag.model'; + +@Module({ + providers: [FeatureFlagService], + controllers: [FeatureFlagController], + imports: [SequelizeModule.forFeature([Feature, FeatureFlag])], +}) +export class FeatureFlagModule {} diff --git a/src/feature-flag/feature-flag.service.ts b/src/feature-flag/feature-flag.service.ts new file mode 100644 index 0000000..34bbf22 --- /dev/null +++ b/src/feature-flag/feature-flag.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class FeatureFlagService {} diff --git a/src/feature/dto/create-feature.dto.ts b/src/feature/dto/create-feature.dto.ts new file mode 100644 index 0000000..809ffc5 --- /dev/null +++ b/src/feature/dto/create-feature.dto.ts @@ -0,0 +1,4 @@ +export class CreateFeatureDto { + readonly title: string; + readonly description: string; +} diff --git a/src/feature/feature.controller.ts b/src/feature/feature.controller.ts new file mode 100644 index 0000000..e17243d --- /dev/null +++ b/src/feature/feature.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('feature') +export class FeatureController {} diff --git a/src/feature/feature.model.ts b/src/feature/feature.model.ts new file mode 100644 index 0000000..e6cdcdc --- /dev/null +++ b/src/feature/feature.model.ts @@ -0,0 +1,31 @@ +import { Column, DataType, HasOne, Model, Table } from 'sequelize-typescript'; +import { FeatureFlag } from '../feature-flag/feature-flag.model'; + +interface FeatureCreationAttrs { + title: string; + description: string; +} + +@Table({ tableName: 'features' }) +export class Feature extends Model { + @Column({ + type: DataType.INTEGER, + unique: true, + autoIncrement: true, + primaryKey: true, + }) + id: number; + + @Column({ + type: DataType.STRING, + unique: true, + allowNull: false, + }) + title: string; + + @Column({ type: DataType.STRING, allowNull: false }) + description: string; + + @HasOne(() => FeatureFlag) + feature: FeatureFlag; +} diff --git a/src/feature/feature.module.ts b/src/feature/feature.module.ts new file mode 100644 index 0000000..9102482 --- /dev/null +++ b/src/feature/feature.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { FeatureService } from './feature.service'; +import { FeatureController } from './feature.controller'; +import { SequelizeModule } from '@nestjs/sequelize'; +import { FeatureFlag } from '../feature-flag/feature-flag.model'; +import { Feature } from './feature.model'; + +@Module({ + providers: [FeatureService], + controllers: [FeatureController], + imports: [SequelizeModule.forFeature([Feature, FeatureFlag])], +}) +export class FeatureModule {} diff --git a/src/feature/feature.service.ts b/src/feature/feature.service.ts new file mode 100644 index 0000000..4498f7c --- /dev/null +++ b/src/feature/feature.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class FeatureService {}