diff --git a/README.md b/README.md index 08515a9f..463203bd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Scaffolding +# Jumpstart @@ -32,10 +32,4 @@ To run both the frontend and backend with one command: ``` nx run-many -t serve -p frontend backend -``` - -## Other commands - -Run `git submodule update --remote` to pull the latest changes from the component library - -When cloning the repo, make sure to add the `--recurse-modules` flag to also clone the component library submodule (e.g. `git clone --recurse-submodules https://github.com/Code-4-Community/scaffolding.git` for the `scaffolding` repo) +``` \ No newline at end of file diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 54fb044e..8ed15a46 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -1,14 +1,21 @@ +/* import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { TaskModule } from './task/task.module'; import AppDataSource from './data-source'; +// import { TaskModule } from './task/task.module'; +///import { LabelModule } from './label/label.module'; @Module({ - imports: [TypeOrmModule.forRoot(AppDataSource.options), TaskModule], + imports: [ + TypeOrmModule.forRoot(AppDataSource.options), + //TaskModule, + //LabelModule, + ], controllers: [AppController], providers: [AppService], }) export class AppModule {} +*/ \ No newline at end of file diff --git a/apps/backend/src/auth/auth.controller.spec.ts b/apps/backend/src/auth/auth.controller.spec.ts deleted file mode 100644 index 27a31e61..00000000 --- a/apps/backend/src/auth/auth.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthController } from './auth.controller'; - -describe('AuthController', () => { - let controller: AuthController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AuthController], - }).compile(); - - controller = module.get(AuthController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts deleted file mode 100644 index bb04b6b7..00000000 --- a/apps/backend/src/auth/auth.controller.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - BadRequestException, - Body, - Controller, - Post, - Request, - UseGuards, -} from '@nestjs/common'; - -import { SignInDto } from './dtos/sign-in.dto'; -import { SignUpDto } from './dtos/sign-up.dto'; -import { AuthService } from './auth.service'; -import { UsersService } from '../users/users.service'; -import { VerifyUserDto } from './dtos/verify-user.dto'; -import { DeleteUserDto } from './dtos/delete-user.dto'; -import { User } from '../users/user.entity'; -import { SignInResponseDto } from './dtos/sign-in-response.dto'; -import { RefreshTokenDto } from './dtos/refresh-token.dto'; -import { AuthGuard } from '@nestjs/passport'; -import { ConfirmPasswordDto } from './dtos/confirm-password.dto'; -import { ForgotPasswordDto } from './dtos/forgot-password.dto'; -import { ApiTags } from '@nestjs/swagger'; - -@ApiTags('Auth') -@Controller('auth') -export class AuthController { - constructor( - private authService: AuthService, - private usersService: UsersService, - ) {} - - @Post('/signup') - async createUser(@Body() signUpDto: SignUpDto): Promise { - // By default, creates a standard user - try { - await this.authService.signup(signUpDto); - } catch (e) { - throw new BadRequestException(e.message); - } - - const user = await this.usersService.create( - signUpDto.email, - signUpDto.firstName, - signUpDto.lastName, - ); - - return user; - } - - // TODO deprecated if verification code is replaced by link - @Post('/verify') - verifyUser(@Body() body: VerifyUserDto): void { - try { - this.authService.verifyUser(body.email, body.verificationCode); - } catch (e) { - throw new BadRequestException(e.message); - } - } - - @Post('/signin') - signin(@Body() signInDto: SignInDto): Promise { - return this.authService.signin(signInDto); - } - - @Post('/refresh') - refresh(@Body() refreshDto: RefreshTokenDto): Promise { - return this.authService.refreshToken(refreshDto); - } - - @Post('/forgotPassword') - forgotPassword(@Body() body: ForgotPasswordDto): Promise { - return this.authService.forgotPassword(body.email); - } - - @Post('/confirmPassword') - confirmPassword(@Body() body: ConfirmPasswordDto): Promise { - return this.authService.confirmForgotPassword(body); - } - - @Post('/delete') - async delete(@Body() body: DeleteUserDto): Promise { - const user = await this.usersService.findOne(body.userId); - - try { - await this.authService.deleteUser(user.email); - } catch (e) { - throw new BadRequestException(e.message); - } - - this.usersService.remove(user.id); - } -} diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts deleted file mode 100644 index 09f5965c..00000000 --- a/apps/backend/src/auth/auth.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 { UsersService } from '../users/users.service'; -import { User } from '../users/user.entity'; -import { JwtStrategy } from './jwt.strategy'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([User]), - PassportModule.register({ defaultStrategy: 'jwt' }), - ], - controllers: [AuthController], - providers: [AuthService, UsersService, JwtStrategy], -}) -export class AuthModule {} diff --git a/apps/backend/src/auth/auth.service.spec.ts b/apps/backend/src/auth/auth.service.spec.ts deleted file mode 100644 index 800ab662..00000000 --- a/apps/backend/src/auth/auth.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthService } from './auth.service'; - -describe('AuthService', () => { - let service: AuthService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [AuthService], - }).compile(); - - service = module.get(AuthService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts deleted file mode 100644 index d78a12dd..00000000 --- a/apps/backend/src/auth/auth.service.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - AdminDeleteUserCommand, - AdminInitiateAuthCommand, - AttributeType, - CognitoIdentityProviderClient, - ConfirmForgotPasswordCommand, - ConfirmSignUpCommand, - ForgotPasswordCommand, - ListUsersCommand, - SignUpCommand, -} from '@aws-sdk/client-cognito-identity-provider'; - -import CognitoAuthConfig from './aws-exports'; -import { SignUpDto } from './dtos/sign-up.dto'; -import { SignInDto } from './dtos/sign-in.dto'; -import { SignInResponseDto } from './dtos/sign-in-response.dto'; -import { createHmac } from 'crypto'; -import { RefreshTokenDto } from './dtos/refresh-token.dto'; -import { Status } from '../users/types'; -import { ConfirmPasswordDto } from './dtos/confirm-password.dto'; - -@Injectable() -export class AuthService { - private readonly providerClient: CognitoIdentityProviderClient; - private readonly clientSecret: string; - - constructor() { - this.providerClient = new CognitoIdentityProviderClient({ - region: CognitoAuthConfig.region, - credentials: { - accessKeyId: process.env.NX_AWS_ACCESS_KEY, - secretAccessKey: process.env.NX_AWS_SECRET_ACCESS_KEY, - }, - }); - - this.clientSecret = process.env.COGNITO_CLIENT_SECRET; - } - - // Computes secret hash to authenticate this backend to Cognito - // Hash key is the Cognito client secret, message is username + client ID - // Username value depends on the command - // (see https://docs.aws.amazon.com/cognito/latest/developerguide/signing-up-users-in-your-app.html#cognito-user-pools-computing-secret-hash) - calculateHash(username: string): string { - const hmac = createHmac('sha256', this.clientSecret); - hmac.update(username + CognitoAuthConfig.clientId); - return hmac.digest('base64'); - } - - async getUser(userSub: string): Promise { - const listUsersCommand = new ListUsersCommand({ - UserPoolId: CognitoAuthConfig.userPoolId, - Filter: `sub = "${userSub}"`, - }); - - // TODO need error handling - const { Users } = await this.providerClient.send(listUsersCommand); - return Users[0].Attributes; - } - - async signup( - { firstName, lastName, email, password }: SignUpDto, - status: Status = Status.STANDARD, - ): Promise { - // Needs error handling - const signUpCommand = new SignUpCommand({ - ClientId: CognitoAuthConfig.clientId, - SecretHash: this.calculateHash(email), - Username: email, - Password: password, - UserAttributes: [ - { - Name: 'name', - Value: `${firstName} ${lastName}`, - }, - // Optional: add a custom Cognito attribute called "role" that also stores the user's status/role - // If you choose to do so, you'll have to first add this custom attribute in your user pool - { - Name: 'custom:role', - Value: status, - }, - ], - }); - - const response = await this.providerClient.send(signUpCommand); - return response.UserConfirmed; - } - - async verifyUser(email: string, verificationCode: string): Promise { - const confirmCommand = new ConfirmSignUpCommand({ - ClientId: CognitoAuthConfig.clientId, - SecretHash: this.calculateHash(email), - Username: email, - ConfirmationCode: verificationCode, - }); - - await this.providerClient.send(confirmCommand); - } - - async signin({ email, password }: SignInDto): Promise { - const signInCommand = new AdminInitiateAuthCommand({ - AuthFlow: 'ADMIN_USER_PASSWORD_AUTH', - ClientId: CognitoAuthConfig.clientId, - UserPoolId: CognitoAuthConfig.userPoolId, - AuthParameters: { - USERNAME: email, - PASSWORD: password, - SECRET_HASH: this.calculateHash(email), - }, - }); - - const response = await this.providerClient.send(signInCommand); - - return { - accessToken: response.AuthenticationResult.AccessToken, - refreshToken: response.AuthenticationResult.RefreshToken, - idToken: response.AuthenticationResult.IdToken, - }; - } - - // Refresh token hash uses a user's sub (unique ID), not their username (typically their email) - async refreshToken({ - refreshToken, - userSub, - }: RefreshTokenDto): Promise { - const refreshCommand = new AdminInitiateAuthCommand({ - AuthFlow: 'REFRESH_TOKEN_AUTH', - ClientId: CognitoAuthConfig.clientId, - UserPoolId: CognitoAuthConfig.userPoolId, - AuthParameters: { - REFRESH_TOKEN: refreshToken, - SECRET_HASH: this.calculateHash(userSub), - }, - }); - - const response = await this.providerClient.send(refreshCommand); - - return { - accessToken: response.AuthenticationResult.AccessToken, - refreshToken: refreshToken, - idToken: response.AuthenticationResult.IdToken, - }; - } - - async forgotPassword(email: string) { - const forgotCommand = new ForgotPasswordCommand({ - ClientId: CognitoAuthConfig.clientId, - Username: email, - SecretHash: this.calculateHash(email), - }); - - await this.providerClient.send(forgotCommand); - } - - async confirmForgotPassword({ - email, - confirmationCode, - newPassword, - }: ConfirmPasswordDto) { - const confirmComamnd = new ConfirmForgotPasswordCommand({ - ClientId: CognitoAuthConfig.clientId, - SecretHash: this.calculateHash(email), - Username: email, - ConfirmationCode: confirmationCode, - Password: newPassword, - }); - - await this.providerClient.send(confirmComamnd); - } - - async deleteUser(email: string): Promise { - const adminDeleteUserCommand = new AdminDeleteUserCommand({ - Username: email, - UserPoolId: CognitoAuthConfig.userPoolId, - }); - - await this.providerClient.send(adminDeleteUserCommand); - } -} diff --git a/apps/backend/src/auth/aws-exports.ts b/apps/backend/src/auth/aws-exports.ts deleted file mode 100644 index 5c3a2dec..00000000 --- a/apps/backend/src/auth/aws-exports.ts +++ /dev/null @@ -1,7 +0,0 @@ -const CognitoAuthConfig = { - userPoolId: 'USER POOL ID HERE', - clientId: 'CLIENT ID HERE', - region: 'us-east-2', -}; - -export default CognitoAuthConfig; diff --git a/apps/backend/src/auth/dtos/confirm-password.dto.ts b/apps/backend/src/auth/dtos/confirm-password.dto.ts deleted file mode 100644 index ec1d63bb..00000000 --- a/apps/backend/src/auth/dtos/confirm-password.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsEmail, IsString } from 'class-validator'; - -export class ConfirmPasswordDto { - @IsEmail() - email: string; - - @IsString() - newPassword: string; - - @IsString() - confirmationCode: string; -} diff --git a/apps/backend/src/auth/dtos/delete-user.dto.ts b/apps/backend/src/auth/dtos/delete-user.dto.ts deleted file mode 100644 index 1a616376..00000000 --- a/apps/backend/src/auth/dtos/delete-user.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsPositive } from 'class-validator'; - -export class DeleteUserDto { - @IsPositive() - userId: number; -} diff --git a/apps/backend/src/auth/dtos/forgot-password.dto.ts b/apps/backend/src/auth/dtos/forgot-password.dto.ts deleted file mode 100644 index bbedf083..00000000 --- a/apps/backend/src/auth/dtos/forgot-password.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsEmail } from 'class-validator'; - -export class ForgotPasswordDto { - @IsEmail() - email: string; -} diff --git a/apps/backend/src/auth/dtos/refresh-token.dto.ts b/apps/backend/src/auth/dtos/refresh-token.dto.ts deleted file mode 100644 index f67905d3..00000000 --- a/apps/backend/src/auth/dtos/refresh-token.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IsString } from 'class-validator'; - -export class RefreshTokenDto { - @IsString() - refreshToken: string; - - @IsString() - userSub: string; -} diff --git a/apps/backend/src/auth/dtos/sign-in-response.dto.ts b/apps/backend/src/auth/dtos/sign-in-response.dto.ts deleted file mode 100644 index 571a02f8..00000000 --- a/apps/backend/src/auth/dtos/sign-in-response.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -export class SignInResponseDto { - /** - * The JWT access token to be passed in API requests - * @example eyJ... - */ - accessToken: string; - - /** - * The JWT refresh token to maintain user sessions by requesting new access tokens - * @example eyJ... - */ - refreshToken: string; - - /** - * The JWT ID token that carries the user's information - * @example eyJ... - */ - idToken: string; -} diff --git a/apps/backend/src/auth/dtos/sign-in.dto.ts b/apps/backend/src/auth/dtos/sign-in.dto.ts deleted file mode 100644 index 51cd9c95..00000000 --- a/apps/backend/src/auth/dtos/sign-in.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IsEmail, IsString } from 'class-validator'; - -export class SignInDto { - @IsEmail() - email: string; - - @IsString() - password: string; -} diff --git a/apps/backend/src/auth/dtos/sign-up.dto.ts b/apps/backend/src/auth/dtos/sign-up.dto.ts deleted file mode 100644 index 5756f186..00000000 --- a/apps/backend/src/auth/dtos/sign-up.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IsEmail, IsString } from 'class-validator'; - -export class SignUpDto { - @IsString() - firstName: string; - - @IsString() - lastName: string; - - @IsEmail() - email: string; - - @IsString() - password: string; -} diff --git a/apps/backend/src/auth/dtos/verify-user.dto.ts b/apps/backend/src/auth/dtos/verify-user.dto.ts deleted file mode 100644 index 66391605..00000000 --- a/apps/backend/src/auth/dtos/verify-user.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IsEmail, IsString } from 'class-validator'; - -export class VerifyUserDto { - @IsEmail() - email: string; - - @IsString() - verificationCode: string; -} diff --git a/apps/backend/src/auth/jwt.strategy.ts b/apps/backend/src/auth/jwt.strategy.ts deleted file mode 100644 index 44d8789d..00000000 --- a/apps/backend/src/auth/jwt.strategy.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { passportJwtSecret } from 'jwks-rsa'; -import { ExtractJwt, Strategy } from 'passport-jwt'; - -import CognitoAuthConfig from './aws-exports'; - -@Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { - constructor() { - const cognitoAuthority = `https://cognito-idp.${CognitoAuthConfig.region}.amazonaws.com/${CognitoAuthConfig.userPoolId}`; - - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, - _audience: CognitoAuthConfig.clientId, - issuer: cognitoAuthority, - algorithms: ['RS256'], - secretOrKeyProvider: passportJwtSecret({ - cache: true, - rateLimit: true, - jwksRequestsPerMinute: 5, - jwksUri: cognitoAuthority + '/.well-known/jwks.json', - }), - }); - } - - async validate(payload) { - return { idUser: payload.sub, email: payload.email }; - } -} diff --git a/apps/backend/src/data-source.ts b/apps/backend/src/data-source.ts index 4cd06624..dff1bf6a 100644 --- a/apps/backend/src/data-source.ts +++ b/apps/backend/src/data-source.ts @@ -1,6 +1,7 @@ import { DataSource } from 'typeorm'; import { PluralNamingStrategy } from './strategies/plural-naming.strategy'; import { Task } from './task/types/task.entity'; +import { Label } from './label/types/label.entity'; import * as dotenv from 'dotenv'; dotenv.config(); @@ -12,7 +13,7 @@ const AppDataSource = new DataSource({ username: process.env.NX_DB_USERNAME, password: process.env.NX_DB_PASSWORD, database: process.env.NX_DB_DATABASE, - entities: [Task], + //entities: [Task, Label], migrations: ['apps/backend/src/migrations/*.js'], // Setting synchronize: true shouldn't be used in production - otherwise you can lose production data synchronize: false, diff --git a/apps/backend/src/interceptors/current-user.interceptor.ts b/apps/backend/src/interceptors/current-user.interceptor.ts deleted file mode 100644 index 3d5d8297..00000000 --- a/apps/backend/src/interceptors/current-user.interceptor.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - Injectable, - NestInterceptor, - ExecutionContext, - CallHandler, -} from '@nestjs/common'; -import { AuthService } from '../auth/auth.service'; -import { UsersService } from '../users/users.service'; - -@Injectable() -export class CurrentUserInterceptor implements NestInterceptor { - constructor( - private authService: AuthService, - private usersService: UsersService, - ) {} - - async intercept(context: ExecutionContext, handler: CallHandler) { - const request = context.switchToHttp().getRequest(); - const cognitoUserAttributes = await this.authService.getUser( - request.user.idUser, - ); - const userEmail = cognitoUserAttributes.find( - (attribute) => attribute.Name === 'email', - ).Value; - const users = await this.usersService.find(userEmail); - - if (users.length > 0) { - const user = users[0]; - - request.user = user; - } - - return handler.handle(); - } -} diff --git a/apps/backend/src/label/dtos/create-label.dto.ts b/apps/backend/src/label/dtos/create-label.dto.ts new file mode 100644 index 00000000..f2a0ea8f --- /dev/null +++ b/apps/backend/src/label/dtos/create-label.dto.ts @@ -0,0 +1,4 @@ +// should include a name (string) and hex color (string) +export class CreateLabelDTO { + +} \ No newline at end of file diff --git a/apps/backend/src/label/dtos/update-single-label.dto.ts b/apps/backend/src/label/dtos/update-single-label.dto.ts new file mode 100644 index 00000000..4f18c769 --- /dev/null +++ b/apps/backend/src/label/dtos/update-single-label.dto.ts @@ -0,0 +1,3 @@ +// should include an optional name (string) and optional hex color (string) +export class UpdateSingleLabelDTO { +} \ No newline at end of file diff --git a/apps/backend/src/label/label.controller.spec.ts b/apps/backend/src/label/label.controller.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/backend/src/label/label.controller.ts b/apps/backend/src/label/label.controller.ts new file mode 100644 index 00000000..b160a917 --- /dev/null +++ b/apps/backend/src/label/label.controller.ts @@ -0,0 +1,71 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { LabelsService } from './label.service'; +import { Label } from './types/label.entity'; +import { CreateLabelDTO } from './dtos/create-label.dto'; +import { UpdateSingleLabelDTO } from './dtos/update-single-label.dto'; + +@ApiTags('labels') +@Controller('labels') +export class LabelsController { + constructor(private readonly labelsService: LabelsService) {} + + /** Creates a new label. + * @param LabelDto - The data transfer object containing label details. + * @returns The created label. + * @throws BadRequestException if the label name is not unique + * @throws BadRequestException if label name is not provided + * @throws BadRequestException if color is not provided or is not hexadecimal + */ + + @Post('/label') + async createLabel(@Body() labelDto: CreateLabelDTO): Promise