auth
This commit is contained in:
@@ -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!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { LoginDTO } from './model/dto/login.dto';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
@@ -9,4 +10,9 @@ export class AppController {
|
||||
getHello(): any {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@Post()
|
||||
login(@Body() createUserDto: LoginDTO) {
|
||||
return { success: createUserDto };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DatabaseModule } from './shared/database/database.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -10,28 +11,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
envFilePath: ['.env'],
|
||||
isGlobal: true,
|
||||
}),
|
||||
TypeOrmModule.forRootAsync({
|
||||
useFactory: () => ({
|
||||
type: 'mysql',
|
||||
host: process.env.DATABASE_HOST,
|
||||
port: parseInt(process.env.DATABASE_PORT) || 3306,
|
||||
username: process.env.MYSQL_USER,
|
||||
password: process.env.MYSQL_PASSWORD,
|
||||
database: process.env.MYSQL_DATABASE,
|
||||
synchronize: true,
|
||||
autoLoadEntities: true,
|
||||
retryAttempts: 5,
|
||||
retryDelay: 10000,
|
||||
logging: ['error'],
|
||||
logger: 'file',
|
||||
}),
|
||||
}),
|
||||
DatabaseModule,
|
||||
AuthModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {
|
||||
constructor(private config: ConfigService) {
|
||||
console.log(this.config.get('MYSQL_USER'))
|
||||
}
|
||||
}
|
||||
export class AppModule {}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
constructor() {}
|
||||
getHello(): any {
|
||||
return { success: true, date: new Date() };
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestFactory, Reflector } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
await app.listen(4000);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
6
api/src/model/dto/auth-code.dto.ts
Normal file
6
api/src/model/dto/auth-code.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class AuthCodeDto {
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
}
|
||||
9
api/src/model/dto/create-user.dto.ts
Normal file
9
api/src/model/dto/create-user.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { IsEmail, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsEmail()
|
||||
username: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
externalId: string;
|
||||
}
|
||||
2
api/src/model/dto/index.ts
Normal file
2
api/src/model/dto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './login.dto';
|
||||
export * from './auth-code.dto';
|
||||
9
api/src/model/dto/login.dto.ts
Normal file
9
api/src/model/dto/login.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { IsEmail, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class LoginDTO {
|
||||
@IsEmail()
|
||||
username: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
||||
2
api/src/model/entitites/index.ts
Normal file
2
api/src/model/entitites/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './sso.user.entity';
|
||||
export * from './user.entity';
|
||||
18
api/src/model/entitites/sso.user.entity.ts
Normal file
18
api/src/model/entitites/sso.user.entity.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
|
||||
@Entity()
|
||||
export class SSOUser {
|
||||
@PrimaryColumn({ type: 'uuid', unique: true })
|
||||
externalId: string;
|
||||
|
||||
@OneToOne(() => User, (user) => user.external)
|
||||
@JoinColumn()
|
||||
user: User;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
accessToken: string;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
refreshToken: string;
|
||||
}
|
||||
44
api/src/model/entitites/user.entity.ts
Normal file
44
api/src/model/entitites/user.entity.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Exclude } from 'class-transformer';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { IUser } from '../interface';
|
||||
import { SSOUser } from './sso.user.entity';
|
||||
import { IsEmail } from 'class-validator';
|
||||
|
||||
@Entity()
|
||||
export class User implements IUser {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@IsEmail()
|
||||
@Column({ unique: true })
|
||||
username: string;
|
||||
|
||||
@Column({ name: 'first_name', default: '' })
|
||||
firstName: string;
|
||||
|
||||
@Column({ name: 'last_name', default: '' })
|
||||
lastName: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@Column({ default: null })
|
||||
lastLogin: Date;
|
||||
|
||||
@Exclude()
|
||||
@OneToOne(() => SSOUser, (sso) => sso.user, { eager: true, cascade: true })
|
||||
external: SSOUser;
|
||||
|
||||
@Exclude()
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface IExternalAccessPayload {
|
||||
username: string;
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
iss: string;
|
||||
aud: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
3
api/src/model/interface/index.ts
Normal file
3
api/src/model/interface/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './user.interface';
|
||||
export * from './external-access-token.payload.interface';
|
||||
export * from './payload.interface';
|
||||
5
api/src/model/interface/payload.interface.ts
Normal file
5
api/src/model/interface/payload.interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface IPayload {
|
||||
id: string;
|
||||
username: string;
|
||||
type: 'access' | 'refresh';
|
||||
}
|
||||
10
api/src/model/interface/user.interface.ts
Normal file
10
api/src/model/interface/user.interface.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface IUser {
|
||||
id: string;
|
||||
username: string;
|
||||
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
}
|
||||
2
api/src/model/repositories/index.ts
Normal file
2
api/src/model/repositories/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './user.repository';
|
||||
export * from './ssouser.repository';
|
||||
18
api/src/model/repositories/ssouser.repository.ts
Normal file
18
api/src/model/repositories/ssouser.repository.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { SSOUser } from '../entitites';
|
||||
|
||||
@Injectable()
|
||||
export class SsoUserRepository extends Repository<SSOUser> {
|
||||
constructor(dataSource: DataSource) {
|
||||
super(SSOUser, dataSource.createEntityManager());
|
||||
}
|
||||
|
||||
findOneByUserId(id: string): Promise<SSOUser> {
|
||||
return this.findOne({ where: { user: { id: id } } });
|
||||
}
|
||||
|
||||
findByExternalId(id: string): Promise<SSOUser> {
|
||||
return this.findOne({ where: { externalId: id } });
|
||||
}
|
||||
}
|
||||
61
api/src/model/repositories/user.repository.ts
Normal file
61
api/src/model/repositories/user.repository.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { User } from '../entitites';
|
||||
import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { SsoUserRepository } from './ssouser.repository';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository extends Repository<User> {
|
||||
constructor(
|
||||
dataSource: DataSource,
|
||||
private ssoRepo: SsoUserRepository,
|
||||
) {
|
||||
super(User, dataSource.createEntityManager());
|
||||
}
|
||||
|
||||
findByUsername(username: string): Promise<User> {
|
||||
return this.findOne({ where: { username }, relations: ['external'] });
|
||||
}
|
||||
|
||||
findById(id: string): Promise<User> {
|
||||
return this.findOne({ where: { id }, relations: ['external'] });
|
||||
}
|
||||
|
||||
async createUser(createUserDto: CreateUserDto): Promise<User> {
|
||||
if (
|
||||
!(await this.checkIfCanInserted(
|
||||
createUserDto.username,
|
||||
createUserDto.externalId,
|
||||
))
|
||||
) {
|
||||
throw new HttpException('user_exists', HttpStatus.UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
try {
|
||||
const created = this.create(createUserDto);
|
||||
const sso = this.ssoRepo.create({
|
||||
user: created,
|
||||
externalId: createUserDto.externalId,
|
||||
});
|
||||
created.external = sso;
|
||||
const user = await this.save(created);
|
||||
sso.user = user;
|
||||
this.ssoRepo.save(sso);
|
||||
return user;
|
||||
} catch {
|
||||
throw new HttpException(
|
||||
'not_successfull',
|
||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkIfCanInserted(
|
||||
username: string,
|
||||
externalId: string,
|
||||
): Promise<boolean> {
|
||||
const user = await this.findOneBy({ username });
|
||||
const sso = await this.ssoRepo.findByExternalId(externalId);
|
||||
return user == null && sso == null;
|
||||
}
|
||||
}
|
||||
26
api/src/modules/auth/auth.controller.ts
Normal file
26
api/src/modules/auth/auth.controller.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthCodeDto } from 'src/model/dto';
|
||||
import { User } from 'src/model/entitites';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Post('auth-code')
|
||||
async registerOrLoginWithAuthCode(
|
||||
@Body() authDto: AuthCodeDto,
|
||||
): Promise<User> {
|
||||
const user = await this.authService.registerOrLoginWithAuthCode(authDto);
|
||||
if (user == null) {
|
||||
throw new HttpException('forbidden', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
26
api/src/modules/auth/auth.module.ts
Normal file
26
api/src/modules/auth/auth.module.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { DatabaseModule } from 'src/shared/database/database.module';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
ConfigModule,
|
||||
HttpModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (config: ConfigService) => ({
|
||||
secret: config.get('JWT_SECRET'),
|
||||
signOptions: { expiresIn: config.get('JWT_EXPIRES_IN') },
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
106
api/src/modules/auth/auth.service.ts
Normal file
106
api/src/modules/auth/auth.service.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthCodeDto } from 'src/model/dto';
|
||||
import { CreateUserDto } from 'src/model/dto/create-user.dto';
|
||||
import { UserRepository } from 'src/model/repositories';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { IExternalAccessPayload, IPayload } from 'src/model/interface';
|
||||
import { User } from 'src/model/entitites';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private userRepo: UserRepository,
|
||||
private readonly http: HttpService,
|
||||
private configService: ConfigService,
|
||||
private jwt: JwtService,
|
||||
) {}
|
||||
register(register: CreateUserDto) {
|
||||
return this.userRepo.createUser(register);
|
||||
}
|
||||
|
||||
async registerOrLoginWithAuthCode(auth: AuthCodeDto): Promise<User> {
|
||||
console.log(auth);
|
||||
const body = this.createAuthCodeFormData(auth.code, 'authorization_code');
|
||||
const url = this.configService.get('SSO_TOKEN_URL');
|
||||
return new Promise<User>((resolve) => {
|
||||
this.http.post(url, body).subscribe({
|
||||
next: async (response) => {
|
||||
const user = await this.saveExternalTokens(response.data as any);
|
||||
this.generateTokens(user);
|
||||
resolve(user);
|
||||
},
|
||||
error: () => {
|
||||
resolve(null);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async saveExternalTokens({
|
||||
access_token,
|
||||
refresh_token,
|
||||
}: {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}): Promise<User> {
|
||||
console.log(access_token, refresh_token);
|
||||
const payload: IExternalAccessPayload = this.jwt.decode(access_token);
|
||||
return new Promise<User>(async (resolve) => {
|
||||
let user = await this.userRepo.findByUsername(payload.username);
|
||||
|
||||
if (!user) {
|
||||
user = await this.userRepo.createUser({
|
||||
username: payload.username,
|
||||
externalId: payload.id,
|
||||
});
|
||||
}
|
||||
|
||||
user.firstName = payload.firstName;
|
||||
user.lastName = payload.lastName;
|
||||
|
||||
user.external.accessToken = access_token;
|
||||
user.external.refreshToken = refresh_token;
|
||||
await this.userRepo.save(user);
|
||||
resolve(user);
|
||||
});
|
||||
}
|
||||
|
||||
private generateTokens(user: User) {
|
||||
const payload: IPayload = {
|
||||
username: user.username,
|
||||
id: user.id,
|
||||
type: 'access',
|
||||
};
|
||||
const token = this.jwt.sign(payload);
|
||||
user.accessToken = token;
|
||||
|
||||
const rPay: IPayload = {
|
||||
username: user.username,
|
||||
id: user.id,
|
||||
type: 'refresh',
|
||||
};
|
||||
|
||||
user.refreshToken = this.jwt.sign(rPay);
|
||||
}
|
||||
|
||||
private createAuthCodeFormData(
|
||||
code: string,
|
||||
grant_type = 'authorization_code',
|
||||
): FormData {
|
||||
const bodyFormData = new FormData();
|
||||
bodyFormData.append('client_id', this.configService.get('SSO_CLIENT_ID'));
|
||||
bodyFormData.append(
|
||||
'client_secret',
|
||||
this.configService.get('SSO_CLIENT_SECRET'),
|
||||
);
|
||||
bodyFormData.append('code', code);
|
||||
bodyFormData.append('grant_type', grant_type);
|
||||
bodyFormData.append(
|
||||
'redirect_uri',
|
||||
this.configService.get('SSO_REDIRECT_URI'),
|
||||
);
|
||||
return bodyFormData;
|
||||
}
|
||||
}
|
||||
32
api/src/shared/database/database.module.ts
Normal file
32
api/src/shared/database/database.module.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SSOUser, User } from 'src/model/entitites';
|
||||
import { SsoUserRepository, UserRepository } from 'src/model/repositories';
|
||||
|
||||
const ENTITIES = [User, SSOUser];
|
||||
const REPOSITORIES = [UserRepository, SsoUserRepository];
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRootAsync({
|
||||
useFactory: () => ({
|
||||
type: 'mysql',
|
||||
host: process.env.DATABASE_HOST,
|
||||
port: parseInt(process.env.DATABASE_PORT) || 3306,
|
||||
username: process.env.MYSQL_USER,
|
||||
password: process.env.MYSQL_PASSWORD,
|
||||
database: process.env.MYSQL_DATABASE,
|
||||
synchronize: true,
|
||||
autoLoadEntities: true,
|
||||
retryAttempts: 5,
|
||||
retryDelay: 10000,
|
||||
logging: ['error'],
|
||||
logger: 'file',
|
||||
entities: [...ENTITIES],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
providers: [...REPOSITORIES],
|
||||
exports: [TypeOrmModule, ...REPOSITORIES],
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
Reference in New Issue
Block a user