-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Set up AWS Cognito for log-in #13
base: main
Are you sure you want to change the base?
Changes from all commits
fcd1cda
697d367
416ec4a
f64a9be
04d4896
b11b3ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { | ||
IsArray, | ||
IsBoolean, | ||
IsPositive, | ||
IsEnum, | ||
} from 'class-validator'; | ||
import { Entity, Column } from 'typeorm'; | ||
import { ApplicationStatus } from './types'; | ||
|
||
@Entity() | ||
export class Application { | ||
@Column({ primary: true, generated: true }) | ||
@IsPositive() | ||
applicationId: number; | ||
|
||
@Column() | ||
@IsPositive() | ||
userId: number; | ||
|
||
@Column() | ||
@IsPositive() | ||
featureId: number; | ||
|
||
@Column('varchar') | ||
@IsBoolean() | ||
safety?: boolean; | ||
Comment on lines
+24
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not super familiar with data in Nest, so a couple questions here:
|
||
|
||
@Column() | ||
@IsBoolean() | ||
privacy?: boolean; | ||
|
||
@Column() | ||
@IsBoolean() | ||
release?: boolean; | ||
|
||
@Column({array: true, nullable: false}) | ||
@IsArray() | ||
names: string[]; | ||
|
||
@Column({nullable: false}) | ||
@IsEnum(ApplicationStatus) | ||
status: ApplicationStatus; | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { | ||
Injectable, | ||
BadRequestException, | ||
} from '@nestjs/common'; | ||
import { InjectRepository } from '@nestjs/typeorm'; | ||
import { MongoRepository } from 'typeorm'; | ||
import { Application } from './applications.entity'; | ||
import { ApplicationStatus } from './types'; | ||
import { UserStatus } from '../auth/types'; | ||
|
||
@Injectable() | ||
export class ApplicationsService { | ||
constructor( | ||
@InjectRepository(Application) | ||
private applicationsRepository: MongoRepository<Application>, | ||
) { } | ||
|
||
async create( | ||
userId: number, | ||
featureId: number, | ||
safetyChecked: boolean, | ||
privacyChecked: boolean, | ||
releaseChecked: boolean, | ||
names: string[], | ||
): Promise<Application> { | ||
const userStatus = UserStatus.APPROVED; // replace with actual logic to get user status | ||
if (safetyChecked && privacyChecked && releaseChecked) { | ||
let applicationStatus; | ||
if (userStatus === UserStatus.APPROVED) { | ||
applicationStatus = ApplicationStatus.APPROVED; | ||
} else if (userStatus === UserStatus.PENDING) { | ||
applicationStatus = ApplicationStatus.PENDING; | ||
} else { | ||
throw new BadRequestException('User must be approved or pending approval'); | ||
} | ||
const application = this.applicationsRepository.create({ | ||
userId: userId, | ||
featureId: featureId, | ||
names: names, | ||
status: applicationStatus, | ||
}); | ||
return this.applicationsRepository.save(application); | ||
} | ||
else { | ||
throw new BadRequestException('User must agree to the agreements'); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export enum ApplicationStatus { | ||
PENDING = 'Application Pending', | ||
APPROVED = 'Application Approved', | ||
DENIED = 'Application Denied' | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { | ||
BadRequestException, | ||
Body, | ||
Controller, | ||
Post, | ||
UseInterceptors, | ||
} from '@nestjs/common'; | ||
|
||
import { SignInRequestDto } from './dtos/sign-in.request.dto'; | ||
import { SignUpRequestDTO } from './dtos/sign-up.request.dto'; | ||
import { AuthService } from './auth.service'; | ||
import { SignInResponseDto } from './dtos/sign-in.response.dto'; | ||
import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; | ||
import { ApplicationsService } from '../applications/applications.service'; | ||
import { Application } from '../applications/applications.entity'; | ||
|
||
@Controller('auth') | ||
@UseInterceptors(CurrentUserInterceptor) | ||
export class AuthController { | ||
constructor( | ||
private authService: AuthService, | ||
private applicationsService: ApplicationsService, | ||
) {} | ||
|
||
@Post('/signup') | ||
async createUser(@Body() signUpDto: SignUpRequestDTO): Promise<Application> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we also want to create a user record here through the |
||
|
||
try { | ||
await this.authService.signup(signUpDto); | ||
} catch (e) { | ||
throw new BadRequestException(e.message); | ||
} | ||
|
||
const user = await this.applicationsService.create( | ||
signUpDto.userId, | ||
signUpDto.featureId, | ||
signUpDto.safetyChecked, | ||
signUpDto.privacyChecked, | ||
signUpDto.releaseChecked, | ||
signUpDto.names, | ||
); | ||
|
||
return user; | ||
} | ||
|
||
@Post('/signin') | ||
signin(@Body() signInDto: SignInRequestDto): Promise<SignInResponseDto> { | ||
return this.authService.signin(signInDto); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { TypeOrmModule } from '@nestjs/typeorm'; | ||
import { PassportModule } from '@nestjs/passport'; | ||
|
||
import { AuthController } from './auth.controller'; | ||
import { AuthService } from './auth.service'; | ||
import { ApplicationsService } from '../applications/applications.service'; | ||
import { Application } from '../applications/applications.entity'; | ||
import { JwtStrategy } from './jwt.strategy'; | ||
import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; | ||
|
||
@Module({ | ||
imports: [ | ||
TypeOrmModule.forFeature([Application]), | ||
PassportModule.register({ defaultStrategy: 'jwt' }), | ||
], | ||
controllers: [AuthController], | ||
providers: [AuthService, ApplicationsService, JwtStrategy, CurrentUserInterceptor], | ||
}) | ||
export class AuthModule {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import { BadRequestException, Injectable } from '@nestjs/common'; | ||
import { | ||
AuthenticationDetails, | ||
CognitoUser, | ||
CognitoUserAttribute, | ||
CognitoUserPool, | ||
ISignUpResult, | ||
} from 'amazon-cognito-identity-js'; | ||
import { | ||
AttributeType, | ||
CognitoIdentityProviderClient, | ||
ListUsersCommand, | ||
} from '@aws-sdk/client-cognito-identity-provider'; | ||
|
||
import CognitoAuthConfig from './aws-exports'; | ||
import { SignUpRequestDTO } from './dtos/sign-up.request.dto'; | ||
import { SignInRequestDto } from './dtos/sign-in.request.dto'; | ||
import { SignInResponseDto } from './dtos/sign-in.response.dto'; | ||
|
||
@Injectable() | ||
export class AuthService { | ||
private readonly userPool: CognitoUserPool; | ||
private readonly providerClient: CognitoIdentityProviderClient; | ||
|
||
constructor() { | ||
this.userPool = new CognitoUserPool({ | ||
UserPoolId: CognitoAuthConfig.userPoolId, | ||
ClientId: CognitoAuthConfig.clientId, | ||
}); | ||
|
||
this.providerClient = new CognitoIdentityProviderClient({ | ||
region: CognitoAuthConfig.region, | ||
credentials: { | ||
accessKeyId: process.env.NX_AWS_ACCESS_KEY, | ||
secretAccessKey: process.env.NX_AWS_SECRET_ACCESS_KEY, | ||
}, | ||
}); | ||
} | ||
|
||
async getUserAttributes(userSub: string): Promise<AttributeType[]> { | ||
const listUsersCommand = new ListUsersCommand({ | ||
UserPoolId: CognitoAuthConfig.userPoolId, | ||
Filter: `sub = "${userSub}"`, | ||
}); | ||
|
||
const { Users } = await this.providerClient.send(listUsersCommand); | ||
if (Users.length === 0) { | ||
throw new BadRequestException('The given bearer token is invalid'); | ||
} | ||
|
||
return Users[0].Attributes; | ||
} | ||
|
||
signup({ | ||
firstName, | ||
lastName, | ||
email, | ||
password, | ||
phoneNumber, | ||
zipCode, | ||
birthdate, | ||
}: SignUpRequestDTO): Promise<ISignUpResult> { | ||
return new Promise((resolve, reject) => { | ||
return this.userPool.signUp( | ||
email, | ||
password, | ||
[ | ||
new CognitoUserAttribute({ | ||
Name: 'name', | ||
Value: `${firstName} ${lastName}`, | ||
}), | ||
new CognitoUserAttribute({ | ||
Name: 'phoneNumber', | ||
Value: `${phoneNumber}`, | ||
}), | ||
new CognitoUserAttribute({ | ||
Name: 'zipCode', | ||
Value: `${zipCode}`, | ||
}), | ||
new CognitoUserAttribute({ | ||
Name: 'birthdate', | ||
Value: `${birthdate}`, | ||
}), | ||
], | ||
null, | ||
(err, result) => { | ||
if (err) { | ||
reject(err); | ||
} else { | ||
resolve(result); | ||
} | ||
}, | ||
); | ||
}); | ||
} | ||
|
||
signin({ email, password }: SignInRequestDto): Promise<SignInResponseDto> { | ||
const authenticationDetails = new AuthenticationDetails({ | ||
Username: email, | ||
Password: password, | ||
}); | ||
|
||
const userData = { | ||
Username: email, | ||
Pool: this.userPool, | ||
}; | ||
|
||
const cognitoUser = new CognitoUser(userData); | ||
|
||
return new Promise<SignInResponseDto>((resolve, reject) => { | ||
return cognitoUser.authenticateUser(authenticationDetails, { | ||
onSuccess: (result) => { | ||
resolve({ | ||
accessToken: result.getAccessToken().getJwtToken(), | ||
refreshToken: result.getRefreshToken().getToken(), | ||
}); | ||
}, | ||
onFailure: (err) => { | ||
reject(err); | ||
}, | ||
}); | ||
}); | ||
} | ||
|
||
// findByEmail(email: string): Promise<User[]> { | ||
// return this.userPool.find({ | ||
// where: { email }, | ||
// relations: ['applications'], | ||
// }); | ||
// } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
const CognitoAuthConfig = { | ||
userPoolId: process.env.NX_COGNITO_USER_POOL_ID, | ||
clientId: process.env.NX_COGNITO_CLIENT_ID, | ||
region: 'us-east-2', | ||
}; | ||
|
||
export default CognitoAuthConfig; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { IsEmail, IsString } from 'class-validator'; | ||
|
||
export class SignInRequestDto { | ||
@IsEmail() | ||
email: string; | ||
|
||
@IsString() | ||
password: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export class SignInResponseDto { | ||
accessToken: string; | ||
|
||
refreshToken: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { IsArray, IsBoolean, IsDate, IsEmail, IsNumber, IsPhoneNumber, IsString } from 'class-validator'; | ||
|
||
export class SignUpRequestDTO { | ||
@IsString() | ||
firstName: string; | ||
|
||
@IsString() | ||
lastName: string; | ||
|
||
@IsEmail() | ||
email: string; | ||
|
||
@IsString() | ||
password: string; | ||
|
||
@IsPhoneNumber() | ||
phoneNumber: number; | ||
|
||
@IsNumber() | ||
zipCode: number; | ||
|
||
@IsDate() | ||
birthdate: Date; | ||
|
||
@IsNumber() | ||
userId: number; | ||
|
||
@IsNumber() | ||
featureId: number; | ||
|
||
@IsBoolean() | ||
safetyChecked: boolean; | ||
|
||
@IsBoolean() | ||
privacyChecked: boolean; | ||
|
||
@IsBoolean() | ||
releaseChecked: boolean; | ||
|
||
@IsArray() | ||
names: string[] | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a schema/description of what data we're working with here? I'm curious how this connects to other pieces of data we'll have
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, what does an
Application
represent? Is it someone's application to join the site?