From faf3c42a8dc71c722b07885bbfc7810d1ceba323 Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Thu, 14 Sep 2023 22:56:04 +0200 Subject: [PATCH 01/15] add progress tracking --- src/controllers/index.ts | 1 + src/controllers/user-progress.controller.ts | 227 ++++++++++++++++++++ src/models/index.ts | 1 + src/models/progress.model.ts | 76 +++++++ src/models/user.model.ts | 4 + src/repositories/index.ts | 1 + src/repositories/progress.repository.ts | 16 ++ src/repositories/user.repository.ts | 9 +- 8 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 src/controllers/user-progress.controller.ts create mode 100644 src/models/progress.model.ts create mode 100644 src/repositories/progress.repository.ts diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 04c34f5..8b5353a 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -3,3 +3,4 @@ export * from './ping.controller'; export * from './user-instruction.controller'; export * from './user.controller'; export * from './user-link.controller'; +export * from './user-progress.controller'; diff --git a/src/controllers/user-progress.controller.ts b/src/controllers/user-progress.controller.ts new file mode 100644 index 0000000..c3e9552 --- /dev/null +++ b/src/controllers/user-progress.controller.ts @@ -0,0 +1,227 @@ +import {authenticate} from '@loopback/authentication'; +import {inject} from '@loopback/core'; +import { + repository +} from '@loopback/repository'; +import { + HttpErrors, + del, + get, + param, + patch, + post, + requestBody +} from '@loopback/rest'; +import {SecurityBindings, UserProfile} from '@loopback/security'; +import { + Progress, ProgressRelations +} from '../models'; +import {InstructionRepository, ProgressRepository, UserRepository} from '../repositories'; +import {JWTService} from '../services'; + +export class UserProgressController { + constructor( + @inject('services.jwt.service') + public jwtService: JWTService, + @inject(SecurityBindings.USER, {optional: true}) + public user: UserProfile, + @repository(InstructionRepository) + protected instructionRepository: InstructionRepository, + @repository(UserRepository) protected userRepository: UserRepository, + @repository(UserRepository) protected progressRepository: ProgressRepository, + ) { } + + @authenticate('jwt') + @post('/users/{id}/progresses/{progressId}', { + responses: { + '200': { + description: 'Create Progress of Instruction', + content: { + 'application/json': { + schema: { + type: 'boolean', + }, + }, + }, + }, + }, + }) + async create( + @requestBody({ + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + instructionId: {type: 'number'}, + stepId: {type: 'number'}, + descriptionId: {type: 'number'}, + }, + required: [ + 'instructionId', + 'stepId', + 'descriptionId', + ], + }, + }, + }, + }) + progress: Omit, + ): Promise { + const user = await this.userRepository.findById(this.user.id); + if (!user) { + throw new HttpErrors.NotFound('User not found'); + } + const existingProgress = await this.progressRepository.findOne({ + where: { + instructionId: progress.instructionId, + userId: this.user.id, + }, + }); + if (existingProgress) { + throw new HttpErrors.BadRequest( + 'Progress with this instructionId and userId already exists.', + ); + } + this.progressRepository.create({ + ...progress, + userId: this.user.id, + }); + return true; + } + + @authenticate('jwt') + @patch('/users/{id}/progresses/{instructionId}', { + responses: { + '200': { + description: 'Update Progressof Instruction', + content: { + 'application/json': { + schema: { + type: 'boolean', + }, + }, + }, + }, + }, + }) + async patch( + @param.path.number('instructionId') instructionId: number, + @requestBody({ + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + stepId: {type: 'number'}, + descriptionId: {type: 'number'}, + }, + }, + }, + }, + }) + progress: Partial, + ): Promise { + const userOriginal = await this.userRepository.findById(this.user.id); + if (!userOriginal) { + throw new HttpErrors.NotFound('User not found'); + } + const progressOriginal = await this.progressRepository.findOne({ + where: { + instructionId: instructionId, + userId: this.user.id + }, + }); + if (!progressOriginal) { + throw new HttpErrors.NotFound('Progress not found'); + } + this.validateProgressOwnership(progressOriginal); + await this.instructionRepository.updateById(progressOriginal.id, progress); + return true; + } + + @authenticate('jwt') + @del('/users/{id}/progresses/{instructionId}', { + responses: { + '200': { + description: 'User.Progress DELETE success count', + content: { + 'application/json': { + schema: { + type: 'boolean', + }, + }, + }, + }, + }, + }) + async delete( + @param.query.number('instructionId') instructionId: number, + ): Promise { + const userOriginal = await this.userRepository.findById(this.user.id); + if (!userOriginal) { + throw new HttpErrors.NotFound('User not found'); + } + const progress = await this.progressRepository.findOne({ + where: { + instructionId: instructionId, + userId: this.user.id + }, + }); + if (!progress) { + throw new HttpErrors.NotFound('Progress not found'); + } + this.validateProgressOwnership(progress); + await this.progressRepository.deleteById(progress.id); + return true; + } + + @authenticate('jwt') + @get('/users/{id}/progress/{instructionId}', { + responses: { + '200': { + description: 'Progress model instance', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + id: {type: 'number'}, + instructionId: {type: 'number'}, + stepId: {type: 'number'}, + descriptionId: {type: 'number'}, + userId: {type: 'number'}, + }, + }, + }, + }, + }, + }, + }) + async find( + @param.path.number('instructionId') instructionId: number, + ): Promise<(Progress & ProgressRelations)> { + const user = await this.userRepository.findById(this.user.id); + if (!user) { + throw new HttpErrors.NotFound('User not found'); + } + const progress = await this.progressRepository.findOne({ + where: { + instructionId: instructionId, + userId: this.user.id, + }, + }); + if (!progress) { + throw new HttpErrors.NotFound('Progress not found'); + } + return progress; + } + + private validateProgressOwnership(progress: Progress): void { + if (Number(progress.userId) !== Number(this.user.id)) { + throw new HttpErrors.Forbidden( + 'You are not authorized to this progress', + ); + } + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 51a2391..b3dd252 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -2,3 +2,4 @@ export * from './instruction.model'; export * from './step.model'; export * from './user-link.model'; export * from './user.model'; +export * from './progress.model'; diff --git a/src/models/progress.model.ts b/src/models/progress.model.ts new file mode 100644 index 0000000..03cf810 --- /dev/null +++ b/src/models/progress.model.ts @@ -0,0 +1,76 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model({ + name: 'progress', +}) +export class Progress extends Entity { + @property({ + id: true, + generated: true, + postgresql: { + columnName: 'id', + dataLength: null, + dataPrecision: 10, + dataScale: 0, + nullable: 'NO', + }, + }) + id: number; + + @property({ + type: 'number', + required: true, + postgresql: { + columnName: 'instruction_id', + dataType: 'integer', + dataLength: null, + dataPrecision: null, + dataScale: null, + nullable: 'NO', + }, + }) + instructionId: number; + + @property({ + type: 'number', + required: true, + postgresql: { + columnName: 'step_id', + dataType: 'integer', + dataLength: null, + dataPrecision: null, + dataScale: null, + nullable: 'NO', + }, + }) + stepId: number; + + @property({ + type: 'number', + required: true, + postgresql: { + columnName: 'description_id', + dataType: 'integer', + dataLength: null, + dataPrecision: null, + dataScale: null, + nullable: 'NO', + }, + }) + descriptionId: number; + + @property({ + type: 'number', + }) + userId: number; + + constructor(data?: Partial) { + super(data); + } +} + +export interface ProgressRelations { + // describe navigational properties here +} + +export type ProgressWithRelations = Progress & ProgressRelations; diff --git a/src/models/user.model.ts b/src/models/user.model.ts index a3e584d..b145b7d 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -1,6 +1,7 @@ import {Entity, hasMany, model, property} from '@loopback/repository'; import {UserLink} from '.'; import {Instruction} from './instruction.model'; +import {Progress} from './progress.model'; export enum Language { CZ = 'CZ', @@ -245,6 +246,9 @@ export class User extends Entity { @hasMany(() => Instruction, {keyTo: 'userId'}) instructions: Instruction[]; + @hasMany(() => Progress, {keyTo: 'userId'}) + progresses: Progress[]; + constructor(data?: Partial) { super(data); } diff --git a/src/repositories/index.ts b/src/repositories/index.ts index 68d6cce..9ab65fb 100644 --- a/src/repositories/index.ts +++ b/src/repositories/index.ts @@ -2,3 +2,4 @@ export * from './instruction.repository'; export * from './step.repository'; export * from './user-link.repository'; export * from './user.repository'; +export * from './progress.repository'; diff --git a/src/repositories/progress.repository.ts b/src/repositories/progress.repository.ts new file mode 100644 index 0000000..21722dd --- /dev/null +++ b/src/repositories/progress.repository.ts @@ -0,0 +1,16 @@ +import {inject} from '@loopback/core'; +import {DefaultCrudRepository} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {Progress, ProgressRelations} from '../models'; + +export class ProgressRepository extends DefaultCrudRepository< + Progress, + typeof Progress.prototype.id, + ProgressRelations +> { + constructor( + @inject('datasources.db') dataSource: DbDataSource, + ) { + super(Progress, dataSource); + } +} diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index af68201..2ee7e53 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -5,8 +5,9 @@ import { HasManyRepositoryFactory, } from '@loopback/repository'; import {DbDataSource} from '../datasources'; -import {User, UserRelations, Instruction} from '../models'; +import {User, UserRelations, Instruction, Progress} from '../models'; import {InstructionRepository} from './instruction.repository'; +import {ProgressRepository} from './progress.repository'; export class UserRepository extends DefaultCrudRepository< User, @@ -18,12 +19,16 @@ export class UserRepository extends DefaultCrudRepository< typeof User.prototype.id >; + public readonly progresses: HasManyRepositoryFactory; + constructor( @inject('datasources.db') dataSource: DbDataSource, @repository.getter('InstructionRepository') - protected instructionRepositoryGetter: Getter, + protected instructionRepositoryGetter: Getter, @repository.getter('ProgressRepository') protected progressRepositoryGetter: Getter, ) { super(User, dataSource); + this.progresses = this.createHasManyRepositoryFactoryFor('progresses', progressRepositoryGetter,); + this.registerInclusionResolver('progresses', this.progresses.inclusionResolver); this.instructions = this.createHasManyRepositoryFactoryFor( 'instructions', instructionRepositoryGetter, From e3b2563590b9f60a803223919bdaa99ba2fb1502 Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Thu, 14 Sep 2023 23:02:27 +0200 Subject: [PATCH 02/15] edits --- .../instruction-step.controller.ts | 2 +- .../user-instruction.controller.ts | 9 +++-- src/controllers/user-progress.controller.ts | 37 ++++++++----------- src/repositories/progress.repository.ts | 4 +- src/repositories/user.repository.ts | 19 ++++++++-- 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/controllers/instruction-step.controller.ts b/src/controllers/instruction-step.controller.ts index b17e286..19d7c0a 100644 --- a/src/controllers/instruction-step.controller.ts +++ b/src/controllers/instruction-step.controller.ts @@ -31,7 +31,7 @@ export class InstructionStepController { @repository(InstructionRepository) public instructionRepository: InstructionRepository, @repository(StepRepository) public stepRepository: StepRepository, - ) { } + ) {} @authenticate('jwt') @post('/users/{id}/instructions/{instructionId}/steps/{stepId}', { diff --git a/src/controllers/user-instruction.controller.ts b/src/controllers/user-instruction.controller.ts index 512e780..740dd5f 100644 --- a/src/controllers/user-instruction.controller.ts +++ b/src/controllers/user-instruction.controller.ts @@ -38,7 +38,7 @@ export class UserInstructionController { @repository(InstructionRepository) protected instructionRepository: InstructionRepository, @repository(StepRepository) public stepRepository: StepRepository, - ) { } + ) {} @authenticate('jwt') @post('/users/{id}/instructions/{instructionId}', { @@ -663,10 +663,13 @@ export class UserInstructionController { if (!keyMatch) { throw new HttpErrors.Unauthorized('Invalid password'); } - const instruction = await this.instructionRepository.findById(instructionId); + const instruction = + await this.instructionRepository.findById(instructionId); instruction.premiumUserIds = instruction.premiumUserIds ?? []; if (instruction.premiumUserIds.includes(userId)) { - instruction.premiumUserIds = instruction.premiumUserIds.filter(id => id !== userId); + instruction.premiumUserIds = instruction.premiumUserIds.filter( + id => id !== userId, + ); } else { instruction.premiumUserIds.push(userId); } diff --git a/src/controllers/user-progress.controller.ts b/src/controllers/user-progress.controller.ts index c3e9552..1397ee7 100644 --- a/src/controllers/user-progress.controller.ts +++ b/src/controllers/user-progress.controller.ts @@ -1,8 +1,6 @@ import {authenticate} from '@loopback/authentication'; import {inject} from '@loopback/core'; -import { - repository -} from '@loopback/repository'; +import {repository} from '@loopback/repository'; import { HttpErrors, del, @@ -10,13 +8,15 @@ import { param, patch, post, - requestBody + requestBody, } from '@loopback/rest'; import {SecurityBindings, UserProfile} from '@loopback/security'; +import {Progress, ProgressRelations} from '../models'; import { - Progress, ProgressRelations -} from '../models'; -import {InstructionRepository, ProgressRepository, UserRepository} from '../repositories'; + InstructionRepository, + ProgressRepository, + UserRepository, +} from '../repositories'; import {JWTService} from '../services'; export class UserProgressController { @@ -28,8 +28,9 @@ export class UserProgressController { @repository(InstructionRepository) protected instructionRepository: InstructionRepository, @repository(UserRepository) protected userRepository: UserRepository, - @repository(UserRepository) protected progressRepository: ProgressRepository, - ) { } + @repository(UserRepository) + protected progressRepository: ProgressRepository, + ) {} @authenticate('jwt') @post('/users/{id}/progresses/{progressId}', { @@ -57,11 +58,7 @@ export class UserProgressController { stepId: {type: 'number'}, descriptionId: {type: 'number'}, }, - required: [ - 'instructionId', - 'stepId', - 'descriptionId', - ], + required: ['instructionId', 'stepId', 'descriptionId'], }, }, }, @@ -83,7 +80,7 @@ export class UserProgressController { 'Progress with this instructionId and userId already exists.', ); } - this.progressRepository.create({ + await this.progressRepository.create({ ...progress, userId: this.user.id, }); @@ -129,7 +126,7 @@ export class UserProgressController { const progressOriginal = await this.progressRepository.findOne({ where: { instructionId: instructionId, - userId: this.user.id + userId: this.user.id, }, }); if (!progressOriginal) { @@ -165,7 +162,7 @@ export class UserProgressController { const progress = await this.progressRepository.findOne({ where: { instructionId: instructionId, - userId: this.user.id + userId: this.user.id, }, }); if (!progress) { @@ -200,7 +197,7 @@ export class UserProgressController { }) async find( @param.path.number('instructionId') instructionId: number, - ): Promise<(Progress & ProgressRelations)> { + ): Promise { const user = await this.userRepository.findById(this.user.id); if (!user) { throw new HttpErrors.NotFound('User not found'); @@ -219,9 +216,7 @@ export class UserProgressController { private validateProgressOwnership(progress: Progress): void { if (Number(progress.userId) !== Number(this.user.id)) { - throw new HttpErrors.Forbidden( - 'You are not authorized to this progress', - ); + throw new HttpErrors.Forbidden('You are not authorized to this progress'); } } } diff --git a/src/repositories/progress.repository.ts b/src/repositories/progress.repository.ts index 21722dd..25d0341 100644 --- a/src/repositories/progress.repository.ts +++ b/src/repositories/progress.repository.ts @@ -8,9 +8,7 @@ export class ProgressRepository extends DefaultCrudRepository< typeof Progress.prototype.id, ProgressRelations > { - constructor( - @inject('datasources.db') dataSource: DbDataSource, - ) { + constructor(@inject('datasources.db') dataSource: DbDataSource) { super(Progress, dataSource); } } diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index 2ee7e53..03dafbc 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -19,16 +19,27 @@ export class UserRepository extends DefaultCrudRepository< typeof User.prototype.id >; - public readonly progresses: HasManyRepositoryFactory; + public readonly progresses: HasManyRepositoryFactory< + Progress, + typeof User.prototype.id + >; constructor( @inject('datasources.db') dataSource: DbDataSource, @repository.getter('InstructionRepository') - protected instructionRepositoryGetter: Getter, @repository.getter('ProgressRepository') protected progressRepositoryGetter: Getter, + protected instructionRepositoryGetter: Getter, + @repository.getter('ProgressRepository') + protected progressRepositoryGetter: Getter, ) { super(User, dataSource); - this.progresses = this.createHasManyRepositoryFactoryFor('progresses', progressRepositoryGetter,); - this.registerInclusionResolver('progresses', this.progresses.inclusionResolver); + this.progresses = this.createHasManyRepositoryFactoryFor( + 'progresses', + progressRepositoryGetter, + ); + this.registerInclusionResolver( + 'progresses', + this.progresses.inclusionResolver, + ); this.instructions = this.createHasManyRepositoryFactoryFor( 'instructions', instructionRepositoryGetter, From 9b126d75acdca5a3798ef6ac7d41eca1b027a6c7 Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Thu, 14 Sep 2023 23:15:46 +0200 Subject: [PATCH 03/15] add progress to instruction get --- src/application.ts | 9 +++++++-- src/controllers/user-instruction.controller.ts | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/application.ts b/src/application.ts index 26ad0de..d77bd8f 100644 --- a/src/application.ts +++ b/src/application.ts @@ -11,10 +11,11 @@ import { import {ServiceMixin} from '@loopback/service-proxy'; import * as dotenv from 'dotenv'; import path from 'path'; -import {PingController, UserController} from './controllers'; +import {InstructionStepController, PingController, UserController, UserInstructionController, UserLinkController, UserProgressController} from './controllers'; import {DbDataSource} from './datasources'; import { InstructionRepository, + ProgressRepository, StepRepository, UserLinkRepository, UserRepository, @@ -49,11 +50,15 @@ export class SelecroBackendApplication extends BootMixin( this.component(JWTAuthenticationComponent); this.controller(PingController); this.controller(UserController); - this.controller(StepRepository); + this.controller(UserProgressController); + this.controller(UserLinkController); + this.controller(UserInstructionController); + this.controller(InstructionStepController); this.repository(UserRepository); this.repository(InstructionRepository); this.repository(StepRepository); this.repository(UserLinkRepository); + this.repository(ProgressRepository); this.dataSource(DbDataSource); this.bind('services.jwt.service').toClass(JWTService); diff --git a/src/controllers/user-instruction.controller.ts b/src/controllers/user-instruction.controller.ts index 740dd5f..667c74a 100644 --- a/src/controllers/user-instruction.controller.ts +++ b/src/controllers/user-instruction.controller.ts @@ -15,9 +15,10 @@ import { } from '@loopback/rest'; import {SecurityBindings, UserProfile} from '@loopback/security'; import * as dotenv from 'dotenv'; -import {Difficulty, Instruction} from '../models'; +import {Difficulty, Instruction, Progress, ProgressRelations} from '../models'; import { InstructionRepository, + ProgressRepository, StepRepository, UserRepository, } from '../repositories'; @@ -38,7 +39,9 @@ export class UserInstructionController { @repository(InstructionRepository) protected instructionRepository: InstructionRepository, @repository(StepRepository) public stepRepository: StepRepository, - ) {} + @repository(UserRepository) + protected progressRepository: ProgressRepository, + ) { } @authenticate('jwt') @post('/users/{id}/instructions/{instructionId}', { @@ -277,13 +280,13 @@ export class UserInstructionController { }, }) async getUsersInstructions(): Promise< - Omit[] + {instreuctions: Omit[]; progress: (Progress & ProgressRelations)[];} > { const user = await this.userRepository.findById(this.user.id); if (!user) { throw new HttpErrors.NotFound('User not found'); } - const data = await this.instructionRepository.find({ + const instreuctions = await this.instructionRepository.find({ where: { userId: this.user.id, }, @@ -302,7 +305,12 @@ export class UserInstructionController { premiumUserIds: false, }, }); - return data; + const progress = await this.progressRepository.find({ + where: { + userId: this.user.id, + }, + }); + return {instreuctions, progress}; } @authenticate('jwt') From 674d6c0347605fc4c548d25199541d34f48198f6 Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Thu, 14 Sep 2023 23:33:28 +0200 Subject: [PATCH 04/15] edit --- src/application.ts | 9 ++++++++- src/controllers/user-instruction.controller.ts | 9 +++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/application.ts b/src/application.ts index d77bd8f..0b60130 100644 --- a/src/application.ts +++ b/src/application.ts @@ -11,7 +11,14 @@ import { import {ServiceMixin} from '@loopback/service-proxy'; import * as dotenv from 'dotenv'; import path from 'path'; -import {InstructionStepController, PingController, UserController, UserInstructionController, UserLinkController, UserProgressController} from './controllers'; +import { + InstructionStepController, + PingController, + UserController, + UserInstructionController, + UserLinkController, + UserProgressController, +} from './controllers'; import {DbDataSource} from './datasources'; import { InstructionRepository, diff --git a/src/controllers/user-instruction.controller.ts b/src/controllers/user-instruction.controller.ts index 667c74a..a0ba40d 100644 --- a/src/controllers/user-instruction.controller.ts +++ b/src/controllers/user-instruction.controller.ts @@ -41,7 +41,7 @@ export class UserInstructionController { @repository(StepRepository) public stepRepository: StepRepository, @repository(UserRepository) protected progressRepository: ProgressRepository, - ) { } + ) {} @authenticate('jwt') @post('/users/{id}/instructions/{instructionId}', { @@ -279,9 +279,10 @@ export class UserInstructionController { }, }, }) - async getUsersInstructions(): Promise< - {instreuctions: Omit[]; progress: (Progress & ProgressRelations)[];} - > { + async getUsersInstructions(): Promise<{ + instreuctions: Omit[]; + progress: (Progress & ProgressRelations)[]; + }> { const user = await this.userRepository.findById(this.user.id); if (!user) { throw new HttpErrors.NotFound('User not found'); From 016729bdfce4ec8a9dda3546bec757b08502256d Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Tue, 19 Sep 2023 22:15:32 +0200 Subject: [PATCH 05/15] better vault service --- .github/workflows/continuous-deployment.yml | 61 ++++++---- src/application.ts | 2 +- .../user-instruction.controller.ts | 8 +- src/controllers/user.controller.ts | 112 ++++++++++++++---- src/datasources/db.datasource.ts | 13 +- src/datasources/email.datasource.ts | 8 +- src/index.ts | 5 +- src/services/email.ts | 14 +-- src/services/example-user-policy.hcl | 12 ++ src/services/imgur-service.ts | 4 +- src/services/vault-service.ts | 60 ++++++---- 11 files changed, 205 insertions(+), 94 deletions(-) create mode 100644 src/services/example-user-policy.hcl diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index 6fa2fec..7e30304 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -21,15 +21,23 @@ jobs: - name: Set deployment variables run: | if [ "${{ github.ref }}" = "refs/heads/dev" ]; then - echo "EXTPORT=3002" >> $GITHUB_ENV echo "IMAGE=backend-dev" >> $GITHUB_ENV - echo "VAULT_PORT=8202" >> $GITHUB_ENV - echo "SQLDATABASE=selecro_dev" >> $GITHUB_ENV + echo "DEFAULT_PORT=${{ secrets.DEFAULT_PORT_DEV }}" >> $GITHUB_ENV + echo "SQL_DATABASE=${{ secrets.SQL_DATABASE_DEV }}" >> $GITHUB_ENV + echo "VAULT_PORT=${{ secrets.VAULT_PORT_DEV }}" >> $GITHUB_ENV + echo "UNSEAL_KEY_1=${{ secrets.UNSEAL_KEY_1_DEV }}" >> $GITHUB_ENV + echo "UNSEAL_KEY_2=${{ secrets.UNSEAL_KEY_2_DEV }}" >> $GITHUB_ENV + echo "UNSEAL_KEY_3=${{ secrets.UNSEAL_KEY_3_DEV }}" >> $GITHUB_ENV + echo "ROOT_VAULT_TOKEN=${{ secrets.ROOT_VAULT_TOKEN_DEV }}" >> $GITHUB_ENV elif [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "EXTPORT=3001" >> $GITHUB_ENV echo "IMAGE=backend-main" >> $GITHUB_ENV - echo "VAULT_PORT=8201" >> $GITHUB_ENV - echo "SQLDATABASE=selecro_main" >> $GITHUB_ENV + echo "DEFAULT_PORT=${{ secrets.DEFAULT_PORT_MAIN }}" >> $GITHUB_ENV + echo "SQL_DATABASE=${{ secrets.SQL_DATABASE_MAIN }}" >> $GITHUB_ENV + echo "VAULT_PORT=${{ secrets.VAULT_PORT_MAIN }}" >> $GITHUB_ENV + echo "UNSEAL_KEY_1=${{ secrets.UNSEAL_KEY_1_MAIN }}" >> $GITHUB_ENV + echo "UNSEAL_KEY_2=${{ secrets.UNSEAL_KEY_2_MAIN }}" >> $GITHUB_ENV + echo "UNSEAL_KEY_3=${{ secrets.UNSEAL_KEY_3_MAIN }}" >> $GITHUB_ENV + echo "ROOT_VAULT_TOKEN=${{ secrets.ROOT_VAULT_TOKEN_MAIN }}" >> $GITHUB_ENV else echo "Invalid branch for deployment" && exit 1 fi @@ -78,27 +86,30 @@ jobs: docker ps -a | grep ${{ env.IMAGE }} && docker stop ${{ env.IMAGE }} || true && \ docker ps -a | grep ${{ env.IMAGE }} && docker rm ${{ env.IMAGE }} || true && \ docker run \ - -e HOST="${{ secrets.HOST }}" \ - -e SQLHOST="${{ secrets.SQLHOST }}" \ - -e SQLPORT="${{ secrets.SQLPORT }}" \ - -e SQLUSER="${{ secrets.SQLUSER }}" \ - -e SQLPASSWORD="${{ secrets.SQLPASSWORD }}" \ - -e SQLDATABASE="${{ env.SQLDATABASE }}" \ - -e TOKEN="${{ secrets.TOKEN }}" \ - -e EXTPORT="${{ env.EXTPORT }}" \ - -e EMAILHOST="${{ secrets.EMAILHOST }}" \ - -e EMAILPORT="${{ secrets.EMAILPORT }}" \ - -e EMAILUSER="${{ secrets.EMAILUSER }}" \ - -e EMAILPASSWORD="${{ secrets.EMAILPASSWORD }}" \ - -e JWT_SECRET="$${{ secrets.JWT_SECRET }}" \ - -e UNSEAL_KEY_1="${{ secrets.UNSEAL_KEY_1 }}" \ - -e UNSEAL_KEY_2="${{ secrets.UNSEAL_KEY_2 }}" \ + -e DEFAULT_HOST="${{ secrets.DEFAULT_HOST }}" \ + -e DEFAULT_PORT="${{ env.DEFAULT_PORT }}" \ + -e JWT_SECRET="${{ secrets.JWT_SECRET }}" \ + -e JWT_SECRET_EMAIL="${{ secrets.JWT_SECRET_EMAIL }}" \ + -e JWT_SECRET_SIGNUP="${{ secrets.JWT_SECRET_SIGNUP }}" \ + -e SQL_HOST="${{ secrets.SQL_HOST }}" \ + -e SQL_PORT="${{ secrets.SQL_PORT }}" \ + -e SQL_USER="${{ secrets.SQL_USER }}" \ + -e SQL_PASSWORD="${{ secrets.SQL_PASSWORD }}" \ + -e SQL_DATABASE="${{ env.SQL_DATABASE }}" \ + -e EMAIL_HOST="${{ secrets.EMAIL_HOST }}" \ + -e EMAIL_PORT="${{ secrets.EMAIL_PORT }}" \ + -e EMAIL_USER="${{ secrets.EMAIL_USER }}" \ + -e EMAIL_PASSWORD="${{ secrets.EMAIL_PASSWORD }}" \ + -e VAULT_URL="${{ secrets.VAULT_URL }}" \ -e VAULT_URL="${{ secrets.VAULT_URL }}" \ -e VAULT_PORT="${{ env.VAULT_PORT }}" \ - -e CLIENT_ID="${{ secrets.CLIENT_ID }}" \ - -e ROOT_VAULT="${{ secrets.ROOT_VAULT }}" \ - -e INSTRUCTION_KEY="${{ secrets.INSTRUCTION_KEY }}" \ - -e INSTRUCTION_KEY_PERMISSIONS="${{ secrets.INSTRUCTION_KEY_PERMISSIONS }}" \ + -e UNSEAL_KEY_1="${{ env.UNSEAL_KEY_1 }}" \ + -e UNSEAL_KEY_2="${{ env.UNSEAL_KEY_2 }}" \ + -e UNSEAL_KEY_3="${{ env.UNSEAL_KEY_3 }}" \ + -e ROOT_VAULT_TOKEN="${{ env.ROOT_VAULT_TOKEN }}" \ + -e IMGUR_CLIENT_ID="${{ secrets.IMGUR_CLIENT_ID }}" \ + -e INSTRUCTION_KEY_PREMIUM="${{ secrets.INSTRUCTION_KEY_PREMIUM }}" \ + -e INSTRUCTION_KEY_PREMIUM_PERMISSIONS="${{ secrets.INSTRUCTION_KEY_PREMIUM_PERMISSIONS }}" \ --name ${{ env.IMAGE }} -dp ${{ env.EXTPORT }}:${{ env.EXTPORT }} \ selecro/${{ env.IMAGE }}:${{ github.ref_name }}-${{ env.SHORT_SHA }} && \ docker update --restart unless-stopped ${{ env.IMAGE }} && exit diff --git a/src/application.ts b/src/application.ts index 0b60130..92b207d 100644 --- a/src/application.ts +++ b/src/application.ts @@ -70,7 +70,7 @@ export class SelecroBackendApplication extends BootMixin( this.bind('services.jwt.service').toClass(JWTService); this.bind('authentication.jwt.expiresIn').to('32d'); - this.bind('authentication.jwt.secret').to(process.env.TOKEN); + this.bind('authentication.jwt.secret').to(process.env.JWT_SECRET); this.bind('services.hasher').toClass(BcryptHasher); this.bind('services.hasher.rounds').to(10); this.bind('services.user.service').toClass(MyUserService); diff --git a/src/controllers/user-instruction.controller.ts b/src/controllers/user-instruction.controller.ts index a0ba40d..150381a 100644 --- a/src/controllers/user-instruction.controller.ts +++ b/src/controllers/user-instruction.controller.ts @@ -41,7 +41,7 @@ export class UserInstructionController { @repository(StepRepository) public stepRepository: StepRepository, @repository(UserRepository) protected progressRepository: ProgressRepository, - ) {} + ) { } @authenticate('jwt') @post('/users/{id}/instructions/{instructionId}', { @@ -96,7 +96,7 @@ export class UserInstructionController { if (!key) { throw new HttpErrors.Unauthorized('Key not providen'); } - const instructionKey = process.env.INSTRUCTION_KEY ?? ''; + const instructionKey = process.env.INSTRUCTION_KEY_PREMIUM ?? ''; const keyMatch = await this.hasher.comparePassword(key, instructionKey); if (!keyMatch) { throw new HttpErrors.Unauthorized('Invalid password'); @@ -440,7 +440,7 @@ export class UserInstructionController { if (!instruction) { throw new HttpErrors.NotFound('Instruction not found'); } - const instructionKey = process.env.INSTRUCTION_KEY ?? ''; + const instructionKey = process.env.INSTRUCTION_KEY_PREMIUM ?? ''; const keyMatch = await this.hasher.comparePassword( request.key, instructionKey, @@ -664,7 +664,7 @@ export class UserInstructionController { @param.query.number('instructionId') instructionId: number, @param.query.number('userId') userId: number, ): Promise { - const instructionKey = process.env.INSTRUCTION_KEY_PERMISSIONS ?? ''; + const instructionKey = process.env.INSTRUCTION_KEY_PREMIUM_PERMISSIONS ?? ''; const keyMatch = await this.hasher.comparePassword( request.key, instructionKey, diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index f1f6891..eee0f9d 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -62,7 +62,7 @@ export class UserController { @repository(StepRepository) public stepRepository: StepRepository, @repository(UserLinkRepository) public userLinkRepository: UserLinkRepository, - ) {} + ) { } @post('/login', { responses: { @@ -99,7 +99,7 @@ export class UserController { }, }) credentials: Credentials, - ): Promise<{token: string; tokenKMS: string}> { + ): Promise { const user = await this.userService.verifyCredentials(credentials); const userProfile = this.userService.convertToUserProfile(user); const existingUser = await this.userRepository.findOne({ @@ -109,11 +109,7 @@ export class UserController { throw new HttpErrors.UnprocessableEntity('email is not verified'); } const token = await this.jwtService.generateToken(userProfile); - const tokenKMS = await this.vaultService.authenticate( - String(existingUser.id), - credentials.password, - ); - return {token, tokenKMS}; + return token; } @post('/signup', { @@ -142,7 +138,6 @@ export class UserController { password0: {type: 'string'}, password1: {type: 'string'}, language: {enum: Object.values(Language)}, - wrappedDEK: {type: 'string'}, kekSalt: {type: 'string'}, initializationVector: {type: 'string'}, }, @@ -152,7 +147,6 @@ export class UserController { 'password0', 'password1', 'language', - 'wrappedDEK', 'kekSalt', 'initializationVector', ], @@ -178,26 +172,102 @@ export class UserController { const hashedPassword = await this.hasher.hashPassword( credentials.password0, ); - await this.vaultService.createUser( - String(credentials.id), - credentials.password0, - ); - const tokenKMS = await this.vaultService.authenticate( - String(credentials.id), - credentials.password0, - ); const newUser = new User({ email: credentials.email, username: credentials.username, passwordHash: hashedPassword, - wrappedDEK: credentials.wrappedDEK.toString('base64'), + wrappedDEK: 'null', kekSalt: credentials.kekSalt, initializationVector: credentials.initializationVector.toString('base64'), language: credentials.language, }); const dbUser = await this.userRepository.create(newUser); + await this.vaultService.createUserPolicy( + String(dbUser.id), + ); + await this.vaultService.createUser( + String(dbUser.id), + credentials.password0, + ); + await this.vaultService.createUserKey( + String(dbUser.id), + ); await this.emailService.sendRegistrationEmail(dbUser); - return tokenKMS; + const secret = process.env.JWT_SECRET_SIGNUP ?? ''; + const userId = dbUser.id; + const token = jwt.sign({userId}, secret, { + expiresIn: 60, + algorithm: 'HS256', + }); + return token; + } + + @post('/save-Wrapped-DEK', { + responses: { + '200': { + description: 'Save Wrapped DEK', + content: { + 'application/json': { + schema: { + type: 'boolean', + }, + }, + }, + }, + }, + }) + async saveWrappedDEK( + @requestBody({ + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + token: {type: 'string'}, + wrappedDEK: {type: 'string'}, + }, + required: ['token', 'wrappedDEK'], + }, + }, + }, + }) + request: { + token: string; + wrappedDEK: string; + }, + ): Promise { + interface DecodedToken { + userId: number; + iat: number; + exp: number; + } + try { + const {token} = request; + const secret = process.env.JWT_SECRET_SIGNUP ?? ''; + const decodedToken = jwt.verify(token, secret) as DecodedToken; + const {userId} = decodedToken; + const user = await this.userRepository.findById(userId); + if (!user) { + throw new HttpErrors.UnprocessableEntity('User not found'); + } + if (user.wrappedDEK !== 'null') { + throw new HttpErrors.UnprocessableEntity('DEK already saved'); + } + await this.userRepository.updateById(user.id, {wrappedDEK: request.wrappedDEK}); + return true; + } catch (error) { + if (error.name === 'TokenExpiredError') { + throw new HttpErrors.UnprocessableEntity( + 'Verification token has expired', + ); + } else if (error.name === 'JsonWebTokenError') { + throw new HttpErrors.UnprocessableEntity('Invalid verification token'); + } else { + throw new HttpErrors.UnprocessableEntity( + 'Failed to update user email verification status', + ); + } + } } @post('/verify-email', { @@ -239,7 +309,7 @@ export class UserController { } try { const {token} = request; - const secret = process.env.JWT_SECRET ?? ''; + const secret = process.env.JWT_SECRET_EMAIL ?? ''; const decodedToken = jwt.verify(token, secret) as DecodedToken; const {userId} = decodedToken; const user = await this.userRepository.findById(userId); @@ -349,7 +419,7 @@ export class UserController { exp: number; } try { - const secret = process.env.JWT_SECRET ?? ''; + const secret = process.env.JWT_SECRET_EMAIL ?? ''; const decodedToken = jwt.verify(request.token, secret) as DecodedToken; const {userData} = decodedToken; const user = await this.userRepository.findById(userData); diff --git a/src/datasources/db.datasource.ts b/src/datasources/db.datasource.ts index 0240f77..a276658 100644 --- a/src/datasources/db.datasource.ts +++ b/src/datasources/db.datasource.ts @@ -6,11 +6,11 @@ dotenv.config(); const config = { name: 'db', connector: 'postgresql', - host: process.env.SQLHOST, - port: Number(process.env.SQLPORT), - user: process.env.SQLUSER, - password: process.env.SQLPASSWORD, - database: process.env.SQLDATABASE, + host: process.env.SQL_HOST, + port: Number(process.env.SQL_PORT), + user: process.env.SQL_USER, + password: process.env.SQL_PASSWORD, + database: process.env.SQL_DATABASE, }; // Observe application's life cycle to disconnect the datasource when @@ -20,8 +20,7 @@ const config = { @lifeCycleObserver('datasource') export class DbDataSource extends juggler.DataSource - implements LifeCycleObserver -{ + implements LifeCycleObserver { static dataSourceName = 'db'; static readonly defaultConfig = config; diff --git a/src/datasources/email.datasource.ts b/src/datasources/email.datasource.ts index f92a951..227e42f 100644 --- a/src/datasources/email.datasource.ts +++ b/src/datasources/email.datasource.ts @@ -3,12 +3,12 @@ import * as nodemailer from 'nodemailer'; dotenv.config(); const config = { - host: process.env.EMAILHOST, + host: process.env.EMAIL_HOST, secure: true, - port: Number(process.env.EMAILPORT), + port: Number(process.env.EMAIL_PORT), auth: { - user: process.env.EMAILUSER, - pass: process.env.EMAILPASSWORD, + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASSWORD, }, }; diff --git a/src/index.ts b/src/index.ts index 4187fb3..82da0ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ import * as dotenv from 'dotenv'; import {ApplicationConfig, SelecroBackendApplication} from './application'; -//import {SocketController} from './controllers'; dotenv.config(); export * from './application'; @@ -21,8 +20,8 @@ if (require.main === module) { // Run the application const config = { rest: { - port: +(process.env.EXTPORT ?? 3000), - host: process.env.HOST, + port: +(process.env.DEFAULT_PORT ?? 3000), + host: process.env.DEFAULT_HOST, // The `gracePeriodForClose` provides a graceful close for http/https // servers with keep-alive clients. The default value is `Infinity` // (don't force-close). If you want to immediately destroy all sockets diff --git a/src/services/email.ts b/src/services/email.ts index fb0bc37..b141880 100644 --- a/src/services/email.ts +++ b/src/services/email.ts @@ -8,10 +8,10 @@ dotenv.config(); @bind({scope: BindingScope.TRANSIENT}) export class EmailService { - constructor() {} + constructor() { } public generateVerificationToken(userId: number): string { - const secret = process.env.JWT_SECRET ?? ''; + const secret = process.env.JWT_SECRET_EMAIL ?? ''; const token = jwt.sign({userId}, secret, { expiresIn: '1h', algorithm: 'HS256', @@ -28,7 +28,7 @@ export class EmailService { ); body = body.replace('{{URL}}', url); await EmailDataSource.sendMail({ - from: process.env.EMAILUSER, + from: process.env.EMAIL_USER, to: user.email, subject: 'Selecro: Registration', html: body, @@ -48,13 +48,13 @@ export class EmailService { ); body0 = body0.replace('{{URL}}', url); await EmailDataSource.sendMail({ - from: process.env.EMAILUSER, + from: process.env.EMAIL_USER, to: email, subject: 'Selecro: Email verification', html: body0, }); await EmailDataSource.sendMail({ - from: process.env.EMAILUSER, + from: process.env.EMAIL_USER, to: user.email, subject: 'Selecro: Email change', html: body1, @@ -70,7 +70,7 @@ export class EmailService { ); body = body.replace('{{URL}}', url); await EmailDataSource.sendMail({ - from: process.env.EMAILUSER, + from: process.env.EMAIL_USER, to: user.email, subject: 'Selecro: Change password', html: body, @@ -83,7 +83,7 @@ export class EmailService { 'utf-8', ); await EmailDataSource.sendMail({ - from: process.env.EMAILUSER, + from: process.env.EMAIL_USER, to: user.email, subject: 'Selecro: Successfuly changed password', html: body, diff --git a/src/services/example-user-policy.hcl b/src/services/example-user-policy.hcl new file mode 100644 index 0000000..3db9576 --- /dev/null +++ b/src/services/example-user-policy.hcl @@ -0,0 +1,12 @@ +path "transit/encrypt/{{id}}/*" { + capabilities = ["create", "read"] +} +path "transit/decrypt/{{id}}/*" { + capabilities = ["create", "read"] +} +path "auth/token/renew-self" { + capabilities = ["update"] +} +path "auth/token/create" { + capabilities = ["create"] +} diff --git a/src/services/imgur-service.ts b/src/services/imgur-service.ts index 9a8ca9c..fd25184 100644 --- a/src/services/imgur-service.ts +++ b/src/services/imgur-service.ts @@ -5,9 +5,9 @@ import multer from 'multer'; dotenv.config(); export class ImgurService { - private readonly clientId = process.env.CLIENT_ID ?? ''; + private readonly clientId = process.env.IMGUR_CLIENT_ID ?? ''; - constructor() {} + constructor() { } async savePicture( request: Request, diff --git a/src/services/vault-service.ts b/src/services/vault-service.ts index 8446e32..c4c84e4 100644 --- a/src/services/vault-service.ts +++ b/src/services/vault-service.ts @@ -1,5 +1,6 @@ import fetch from 'cross-fetch'; import * as dotenv from 'dotenv'; +import * as fs from 'fs'; dotenv.config(); export class VaultService { @@ -8,8 +9,9 @@ export class VaultService { private readonly unsealKeys: string[] = [ process.env.UNSEAL_KEY_1 ?? '', process.env.UNSEAL_KEY_2 ?? '', + process.env.UNSEAL_KEY_3 ?? '', ]; - private readonly rootToken = process.env.ROOT_VAULT ?? ''; + private readonly rootToken = process.env.ROOT_VAULT_TOKEN ?? ''; constructor() { this.checkAndUnsealIfNeeded().catch(error => { @@ -21,9 +23,6 @@ export class VaultService { try { const response = await fetch(`${this.vaultEndpoint}/v1/sys/seal-status`, { method: 'GET', - headers: { - 'X-Vault-Token': this.rootToken, - }, }); if (!response.ok) { throw new Error(`Status check error: ${response.statusText}`); @@ -39,7 +38,7 @@ export class VaultService { private async unseal(): Promise { try { - const unsealKeys: string[] = [this.unsealKeys[0], this.unsealKeys[1]]; + const unsealKeys: string[] = [this.unsealKeys[0], this.unsealKeys[1], this.unsealKeys[2]]; for (const key of unsealKeys) { const response = await fetch(`${this.vaultEndpoint}/v1/sys/unseal`, { method: 'POST', @@ -53,7 +52,7 @@ export class VaultService { }); if (!response.ok) { throw new Error( - `Unseal error with key ${key}: ${response.statusText}`, + `Unseal error`, ); } } @@ -62,28 +61,28 @@ export class VaultService { } } - async authenticate(password: string, id: string): Promise { + async createUserPolicy(id: string): Promise { try { - const data = { - password: password, - }; + let policyData = fs.readFileSync( + `./src/services/example-user-policy.hcl`, + 'utf-8', + ); + policyData = policyData.replace('{{id}}', id); const response = await fetch( - `${this.vaultEndpoint}/v1/auth/userpass/login/${id}`, + `${this.vaultEndpoint}/v1/sys/policy/${id}`, { method: 'POST', headers: { - 'Content-Type': 'application/json', + 'X-Vault-Token': this.rootToken, }, - body: JSON.stringify(data), + body: JSON.stringify({data: policyData}), }, ); if (!response.ok) { throw new Error( - `Authentication error: Unable to authenticate with the provided credentials.`, + `Unable to create policy`, ); } - const responseData = await response.json(); - return responseData.auth.client_token; } catch (error) { throw new Error(`Authentication error: ${error.message}`); } @@ -93,7 +92,7 @@ export class VaultService { try { const data = { password: password, - policies: ['selecro-main'], + policies: [String(id)], }; const response = await fetch( `${this.vaultEndpoint}/v1/auth/userpass/users/${id}`, @@ -107,7 +106,28 @@ export class VaultService { ); if (!response.ok) { throw new Error( - `Authentication error: Unable to authenticate with the provided credentials.`, + `Unable to create user`, + ); + } + } catch (error) { + throw new Error(`Authentication error: ${error.message}`); + } + } + + async createUserKey(id: string): Promise { + try { + const response = await fetch( + `${this.vaultEndpoint}/v1/transit/keys/${id}`, + { + method: 'POST', + headers: { + 'X-Vault-Token': this.rootToken, + }, + }, + ); + if (!response.ok) { + throw new Error( + `Unable to create key`, ); } } catch (error) { @@ -132,7 +152,7 @@ export class VaultService { ); if (!response.ok) { throw new Error( - `Authentication error: Unable to authenticate with the provided credentials.`, + `Unable to update password`, ); } } catch (error) { @@ -153,7 +173,7 @@ export class VaultService { ); if (!response.ok) { throw new Error( - `Authentication error: Unable to authenticate with the provided credentials.`, + `Unable to delete user`, ); } } catch (error) { From a0fac10a8073d6ba4fe9a7957bcf8ae9acc7005a Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Tue, 19 Sep 2023 22:16:17 +0200 Subject: [PATCH 06/15] npm fix --- .../user-instruction.controller.ts | 5 ++-- src/controllers/user.controller.ts | 14 ++++----- src/datasources/db.datasource.ts | 3 +- src/services/email.ts | 2 +- src/services/imgur-service.ts | 2 +- src/services/vault-service.ts | 30 +++++++------------ 6 files changed, 24 insertions(+), 32 deletions(-) diff --git a/src/controllers/user-instruction.controller.ts b/src/controllers/user-instruction.controller.ts index 150381a..715149a 100644 --- a/src/controllers/user-instruction.controller.ts +++ b/src/controllers/user-instruction.controller.ts @@ -41,7 +41,7 @@ export class UserInstructionController { @repository(StepRepository) public stepRepository: StepRepository, @repository(UserRepository) protected progressRepository: ProgressRepository, - ) { } + ) {} @authenticate('jwt') @post('/users/{id}/instructions/{instructionId}', { @@ -664,7 +664,8 @@ export class UserInstructionController { @param.query.number('instructionId') instructionId: number, @param.query.number('userId') userId: number, ): Promise { - const instructionKey = process.env.INSTRUCTION_KEY_PREMIUM_PERMISSIONS ?? ''; + const instructionKey = + process.env.INSTRUCTION_KEY_PREMIUM_PERMISSIONS ?? ''; const keyMatch = await this.hasher.comparePassword( request.key, instructionKey, diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index eee0f9d..2c9f0bb 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -62,7 +62,7 @@ export class UserController { @repository(StepRepository) public stepRepository: StepRepository, @repository(UserLinkRepository) public userLinkRepository: UserLinkRepository, - ) { } + ) {} @post('/login', { responses: { @@ -182,16 +182,12 @@ export class UserController { language: credentials.language, }); const dbUser = await this.userRepository.create(newUser); - await this.vaultService.createUserPolicy( - String(dbUser.id), - ); + await this.vaultService.createUserPolicy(String(dbUser.id)); await this.vaultService.createUser( String(dbUser.id), credentials.password0, ); - await this.vaultService.createUserKey( - String(dbUser.id), - ); + await this.vaultService.createUserKey(String(dbUser.id)); await this.emailService.sendRegistrationEmail(dbUser); const secret = process.env.JWT_SECRET_SIGNUP ?? ''; const userId = dbUser.id; @@ -253,7 +249,9 @@ export class UserController { if (user.wrappedDEK !== 'null') { throw new HttpErrors.UnprocessableEntity('DEK already saved'); } - await this.userRepository.updateById(user.id, {wrappedDEK: request.wrappedDEK}); + await this.userRepository.updateById(user.id, { + wrappedDEK: request.wrappedDEK, + }); return true; } catch (error) { if (error.name === 'TokenExpiredError') { diff --git a/src/datasources/db.datasource.ts b/src/datasources/db.datasource.ts index a276658..7f5864d 100644 --- a/src/datasources/db.datasource.ts +++ b/src/datasources/db.datasource.ts @@ -20,7 +20,8 @@ const config = { @lifeCycleObserver('datasource') export class DbDataSource extends juggler.DataSource - implements LifeCycleObserver { + implements LifeCycleObserver +{ static dataSourceName = 'db'; static readonly defaultConfig = config; diff --git a/src/services/email.ts b/src/services/email.ts index b141880..94e4482 100644 --- a/src/services/email.ts +++ b/src/services/email.ts @@ -8,7 +8,7 @@ dotenv.config(); @bind({scope: BindingScope.TRANSIENT}) export class EmailService { - constructor() { } + constructor() {} public generateVerificationToken(userId: number): string { const secret = process.env.JWT_SECRET_EMAIL ?? ''; diff --git a/src/services/imgur-service.ts b/src/services/imgur-service.ts index fd25184..1210266 100644 --- a/src/services/imgur-service.ts +++ b/src/services/imgur-service.ts @@ -7,7 +7,7 @@ dotenv.config(); export class ImgurService { private readonly clientId = process.env.IMGUR_CLIENT_ID ?? ''; - constructor() { } + constructor() {} async savePicture( request: Request, diff --git a/src/services/vault-service.ts b/src/services/vault-service.ts index c4c84e4..94c9f59 100644 --- a/src/services/vault-service.ts +++ b/src/services/vault-service.ts @@ -38,7 +38,11 @@ export class VaultService { private async unseal(): Promise { try { - const unsealKeys: string[] = [this.unsealKeys[0], this.unsealKeys[1], this.unsealKeys[2]]; + const unsealKeys: string[] = [ + this.unsealKeys[0], + this.unsealKeys[1], + this.unsealKeys[2], + ]; for (const key of unsealKeys) { const response = await fetch(`${this.vaultEndpoint}/v1/sys/unseal`, { method: 'POST', @@ -51,9 +55,7 @@ export class VaultService { }), }); if (!response.ok) { - throw new Error( - `Unseal error`, - ); + throw new Error(`Unseal error`); } } } catch (error) { @@ -79,9 +81,7 @@ export class VaultService { }, ); if (!response.ok) { - throw new Error( - `Unable to create policy`, - ); + throw new Error(`Unable to create policy`); } } catch (error) { throw new Error(`Authentication error: ${error.message}`); @@ -105,9 +105,7 @@ export class VaultService { }, ); if (!response.ok) { - throw new Error( - `Unable to create user`, - ); + throw new Error(`Unable to create user`); } } catch (error) { throw new Error(`Authentication error: ${error.message}`); @@ -126,9 +124,7 @@ export class VaultService { }, ); if (!response.ok) { - throw new Error( - `Unable to create key`, - ); + throw new Error(`Unable to create key`); } } catch (error) { throw new Error(`Authentication error: ${error.message}`); @@ -151,9 +147,7 @@ export class VaultService { }, ); if (!response.ok) { - throw new Error( - `Unable to update password`, - ); + throw new Error(`Unable to update password`); } } catch (error) { throw new Error(`Authentication error: ${error.message}`); @@ -172,9 +166,7 @@ export class VaultService { }, ); if (!response.ok) { - throw new Error( - `Unable to delete user`, - ); + throw new Error(`Unable to delete user`); } } catch (error) { throw new Error(`Authentication error: ${error.message}`); From a9ea9852dd6a69d9494efb55777cc85f787d2eff Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Tue, 19 Sep 2023 22:41:19 +0200 Subject: [PATCH 07/15] add delete --- src/controllers/user.controller.ts | 4 +- src/services/vault-service.ts | 72 +++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 2c9f0bb..d8aaace 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -62,7 +62,7 @@ export class UserController { @repository(StepRepository) public stepRepository: StepRepository, @repository(UserLinkRepository) public userLinkRepository: UserLinkRepository, - ) {} + ) { } @post('/login', { responses: { @@ -605,7 +605,9 @@ export class UserController { if (!passwordMatched) { throw new HttpErrors.Unauthorized('Password is not valid'); } + await this.vaultService.deleteUserKey(String(userOriginal.id)); await this.vaultService.deleteUser(String(userOriginal.id)); + await this.vaultService.deleteUserPolicy(String(userOriginal.id)); if (userOriginal.deleteHash) { await this.imgurService.deleteImage(userOriginal.deleteHash); } diff --git a/src/services/vault-service.ts b/src/services/vault-service.ts index 94c9f59..0d34e5d 100644 --- a/src/services/vault-service.ts +++ b/src/services/vault-service.ts @@ -63,49 +63,49 @@ export class VaultService { } } - async createUserPolicy(id: string): Promise { + async createUser(password: string, id: string): Promise { try { - let policyData = fs.readFileSync( - `./src/services/example-user-policy.hcl`, - 'utf-8', - ); - policyData = policyData.replace('{{id}}', id); + const data = { + password: password, + policies: [String(id)], + }; const response = await fetch( - `${this.vaultEndpoint}/v1/sys/policy/${id}`, + `${this.vaultEndpoint}/v1/auth/userpass/users/${id}`, { method: 'POST', headers: { 'X-Vault-Token': this.rootToken, }, - body: JSON.stringify({data: policyData}), + body: JSON.stringify(data), }, ); if (!response.ok) { - throw new Error(`Unable to create policy`); + throw new Error(`Unable to create user`); } } catch (error) { throw new Error(`Authentication error: ${error.message}`); } } - async createUser(password: string, id: string): Promise { + async createUserPolicy(id: string): Promise { try { - const data = { - password: password, - policies: [String(id)], - }; + let policyData = fs.readFileSync( + `./src/services/example-user-policy.hcl`, + 'utf-8', + ); + policyData = policyData.replace('{{id}}', id); const response = await fetch( - `${this.vaultEndpoint}/v1/auth/userpass/users/${id}`, + `${this.vaultEndpoint}/v1/sys/policy/${id}`, { method: 'POST', headers: { 'X-Vault-Token': this.rootToken, }, - body: JSON.stringify(data), + body: JSON.stringify({data: policyData}), }, ); if (!response.ok) { - throw new Error(`Unable to create user`); + throw new Error(`Unable to create policy`); } } catch (error) { throw new Error(`Authentication error: ${error.message}`); @@ -172,4 +172,42 @@ export class VaultService { throw new Error(`Authentication error: ${error.message}`); } } + + async deleteUserPolicy(id: string): Promise { + try { + const response = await fetch( + `${this.vaultEndpoint}/v1/sys/policy/acl/${id}`, + { + method: 'DELETE', + headers: { + 'X-Vault-Token': this.rootToken, + }, + }, + ); + if (!response.ok) { + throw new Error(`Unable to delete policy`); + } + } catch (error) { + throw new Error(`Authentication error: ${error.message}`); + } + } + + async deleteUserKey(id: string): Promise { + try { + const response = await fetch( + `${this.vaultEndpoint}/v1/transit/keys/${id}`, + { + method: 'DELETE', + headers: { + 'X-Vault-Token': this.rootToken, + }, + }, + ); + if (!response.ok) { + throw new Error(`Unable to delete key`); + } + } catch (error) { + throw new Error(`Authentication error: ${error.message}`); + } + } } From 5b7bd96e51f4d45b54258648586e17387ec70bc0 Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Tue, 19 Sep 2023 23:24:18 +0200 Subject: [PATCH 08/15] add refresh token API endpoint --- src/controllers/user.controller.ts | 64 ++++++++++++++++++++++++++++- src/repositories/user.repository.ts | 6 +-- src/services/jwt-service.ts | 39 +++++++++++++++++- src/services/user-service.ts | 2 +- 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index d8aaace..c608b24 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -2,6 +2,7 @@ import {authenticate} from '@loopback/authentication'; import { Credentials, JWTService, + TokenObject, UserCredentials, } from '@loopback/authentication-jwt'; import {inject} from '@loopback/context'; @@ -112,6 +113,64 @@ export class UserController { return token; } + @post('/refresh-token', { + responses: { + '200': { + description: 'Refresh token', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + token: {type: 'string'}, + }, + }, + }, + }, + }, + }, + }) + async refreshToken( + @requestBody({ + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + token: {type: 'string'}, + }, + required: [ + 'token', + ], + }, + }, + }, + }) + requestBody: {refreshToken: string}, + ): Promise { + try { + const {refreshToken} = requestBody; + if (!refreshToken) { + throw new HttpErrors.Unauthorized( + `Error verifying token: 'refresh token' is null`, + ); + } + const userRefreshData = await this.jwtService.verifyToken(refreshToken); + const user = await this.userRepository.findById( + userRefreshData.userId.toString(), + ); + const userProfile: UserProfile = this.userService.convertToUserProfile(user); + const token = await this.jwtService.generateToken(userProfile); + return { + accessToken: token, + }; + } catch (error) { + throw new HttpErrors.Unauthorized( + `Error verifying token: ${error.message}`, + ); + } + } + @post('/signup', { responses: { '200': { @@ -119,7 +178,10 @@ export class UserController { content: { 'application/json': { schema: { - type: 'boolean', + type: 'object', + properties: { + token: {type: 'string'}, + }, }, }, }, diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index 03dafbc..6fca5c3 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -1,11 +1,11 @@ -import {inject, Getter} from '@loopback/core'; +import {Getter, inject} from '@loopback/core'; import { DefaultCrudRepository, - repository, HasManyRepositoryFactory, + repository, } from '@loopback/repository'; import {DbDataSource} from '../datasources'; -import {User, UserRelations, Instruction, Progress} from '../models'; +import {Instruction, Progress, User, UserRelations} from '../models'; import {InstructionRepository} from './instruction.repository'; import {ProgressRepository} from './progress.repository'; diff --git a/src/services/jwt-service.ts b/src/services/jwt-service.ts index 5ab8bc0..c1b9ee8 100644 --- a/src/services/jwt-service.ts +++ b/src/services/jwt-service.ts @@ -4,10 +4,17 @@ // License text available at https://opensource.org/licenses/MIT import {TokenService} from '@loopback/authentication'; +import {TokenObject} from '@loopback/authentication-jwt'; import {inject} from '@loopback/core'; +import {repository} from '@loopback/repository'; import {HttpErrors} from '@loopback/rest'; import {securityId, UserProfile} from '@loopback/security'; import {promisify} from 'util'; +import { + UserRepository +} from '../repositories'; +import {MyUserService} from './user-service'; + const jwt = require('jsonwebtoken'); const signAsync = promisify(jwt.sign); const verifyAsync = promisify(jwt.verify); @@ -18,7 +25,12 @@ export class JWTService implements TokenService { private jwtSecret: string, @inject('authentication.jwt.expiresIn') private jwtExpiresIn: string, - ) {} + @inject('services.user.service') + public userService: MyUserService, + @inject('services.jwt.service') + public jwtService: JWTService, + @repository(UserRepository) protected userRepository: UserRepository, + ) { } async verifyToken(token: string): Promise { if (!token) { @@ -69,4 +81,29 @@ export class JWTService implements TokenService { } return token; } + + async refreshToken(refreshToken: string): Promise { + try { + if (!refreshToken) { + throw new HttpErrors.Unauthorized( + `Error verifying token : 'refresh token' is null`, + ); + } + const userRefreshData = await this.verifyToken(refreshToken); + const user = await this.userRepository.findById( + userRefreshData.userId.toString(), + ); + const userProfile: UserProfile = + this.userService.convertToUserProfile(user); + // create a JSON Web Token based on the user profile + const token = await this.jwtService.generateToken(userProfile); + return { + accessToken: token, + }; + } catch (error) { + throw new HttpErrors.Unauthorized( + `Error verifying token : ${error.message}`, + ); + } + } } diff --git a/src/services/user-service.ts b/src/services/user-service.ts index dca8952..ef78408 100644 --- a/src/services/user-service.ts +++ b/src/services/user-service.ts @@ -14,7 +14,7 @@ export class MyUserService implements UserService { public userRepository: UserRepository, @inject('services.hasher') public hasher: BcryptHasher, - ) {} + ) { } async verifyCredentials(credentials: Credentials): Promise { const foundUser0 = await this.userRepository.findOne({ From 64a48c56929ff3660cb6190618a8f76a229257f4 Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Wed, 20 Sep 2023 19:09:02 +0200 Subject: [PATCH 09/15] add time save --- src/controllers/user-progress.controller.ts | 6 ++++-- src/models/progress.model.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/controllers/user-progress.controller.ts b/src/controllers/user-progress.controller.ts index 1397ee7..64503b5 100644 --- a/src/controllers/user-progress.controller.ts +++ b/src/controllers/user-progress.controller.ts @@ -30,7 +30,7 @@ export class UserProgressController { @repository(UserRepository) protected userRepository: UserRepository, @repository(UserRepository) protected progressRepository: ProgressRepository, - ) {} + ) { } @authenticate('jwt') @post('/users/{id}/progresses/{progressId}', { @@ -57,8 +57,9 @@ export class UserProgressController { instructionId: {type: 'number'}, stepId: {type: 'number'}, descriptionId: {type: 'number'}, + time: {type: 'number'}, }, - required: ['instructionId', 'stepId', 'descriptionId'], + required: ['instructionId', 'stepId', 'descriptionId', 'time'], }, }, }, @@ -112,6 +113,7 @@ export class UserProgressController { properties: { stepId: {type: 'number'}, descriptionId: {type: 'number'}, + time: {type: 'number'}, }, }, }, diff --git a/src/models/progress.model.ts b/src/models/progress.model.ts index 03cf810..ad11bca 100644 --- a/src/models/progress.model.ts +++ b/src/models/progress.model.ts @@ -59,6 +59,20 @@ export class Progress extends Entity { }) descriptionId: number; + @property({ + type: 'number', + required: true, + postgresql: { + columnName: 'time', + dataType: 'integer', + dataLength: null, + dataPrecision: null, + dataScale: null, + nullable: 'NO', + }, + }) + time: number; + @property({ type: 'number', }) From 901f7ff7035d0528c023e5f9f873cb83d469c90a Mon Sep 17 00:00:00 2001 From: Michael Szotkowski Date: Wed, 20 Sep 2023 19:25:15 +0200 Subject: [PATCH 10/15] Update continuous-deployment.yml --- .github/workflows/continuous-deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index 7e30304..055d20f 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -110,7 +110,7 @@ jobs: -e IMGUR_CLIENT_ID="${{ secrets.IMGUR_CLIENT_ID }}" \ -e INSTRUCTION_KEY_PREMIUM="${{ secrets.INSTRUCTION_KEY_PREMIUM }}" \ -e INSTRUCTION_KEY_PREMIUM_PERMISSIONS="${{ secrets.INSTRUCTION_KEY_PREMIUM_PERMISSIONS }}" \ - --name ${{ env.IMAGE }} -dp ${{ env.EXTPORT }}:${{ env.EXTPORT }} \ + --name ${{ env.IMAGE }} -dp ${{ env.DEFAULT_PORT }}:${{ env.DEFAULT_PORT }} \ selecro/${{ env.IMAGE }}:${{ github.ref_name }}-${{ env.SHORT_SHA }} && \ docker update --restart unless-stopped ${{ env.IMAGE }} && exit From cd78e802349d4d1744a22697376cfe394cb76d8f Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Wed, 20 Sep 2023 22:10:43 +0200 Subject: [PATCH 11/15] edit optional parameters --- src/models/instruction.model.ts | 2 +- src/models/user.model.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/instruction.model.ts b/src/models/instruction.model.ts index d539248..891618e 100644 --- a/src/models/instruction.model.ts +++ b/src/models/instruction.model.ts @@ -153,7 +153,7 @@ export class Instruction extends Entity { userId: number; @hasMany(() => Step, {keyTo: 'instructionId'}) - steps: Step[]; + steps?: Step[]; constructor(data?: Partial) { super(data); diff --git a/src/models/user.model.ts b/src/models/user.model.ts index b145b7d..4b3d48c 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -241,13 +241,13 @@ export class User extends Entity { keyTo: 'followeeId', }, }) - users: User[]; + users?: User[]; @hasMany(() => Instruction, {keyTo: 'userId'}) - instructions: Instruction[]; + instructions?: Instruction[]; @hasMany(() => Progress, {keyTo: 'userId'}) - progresses: Progress[]; + progresses?: Progress[]; constructor(data?: Partial) { super(data); From 1e01144c7bccfe68eeaa880ea27ba3b0b2ec1097 Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Wed, 20 Sep 2023 22:18:47 +0200 Subject: [PATCH 12/15] fix --- src/controllers/user.controller.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index c608b24..0489c95 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -524,14 +524,15 @@ export class UserController { id: {type: 'number'}, email: {type: 'string'}, username: {type: 'string'}, + wrappedDEK: {type: 'string'}, + initializationVector: {type: 'string'}, + kekSalt: {type: 'string'}, language: {enum: Object.values(Language)}, - darkmode: {type: 'string'}, - emailVerified: {type: 'string'}, + darkmode: {type: 'boolean'}, date: {type: 'string'}, nick: {type: 'string'}, bio: {type: 'string'}, link: {type: 'string'}, - wrappedDEK: {type: 'string'}, favorites: { type: 'array', items: { @@ -548,14 +549,12 @@ export class UserController { async getUser(): Promise< Omit< User, - 'passwordHash' | 'initializationVector' | 'kekSalt' | 'deleteHash' + 'passwordHash' | 'deleteHash' > > { const user = await this.userRepository.findById(this.user.id, { fields: { passwordHash: false, - initializationVector: false, - kekSalt: false, deleteHash: false, }, }); From d7dae81fe307cdb62b86d9e91cee002bc34a301b Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Thu, 21 Sep 2023 23:43:09 +0200 Subject: [PATCH 13/15] Circular dependency detected is fixed --- .../user-instruction.controller.ts | 121 +++++++++--------- src/controllers/user-link.controller.ts | 112 ++++++++++++++-- src/controllers/user-progress.controller.ts | 12 +- src/controllers/user.controller.ts | 4 +- src/services/jwt-service.ts | 6 +- 5 files changed, 176 insertions(+), 79 deletions(-) diff --git a/src/controllers/user-instruction.controller.ts b/src/controllers/user-instruction.controller.ts index 715149a..87ae7f8 100644 --- a/src/controllers/user-instruction.controller.ts +++ b/src/controllers/user-instruction.controller.ts @@ -41,7 +41,7 @@ export class UserInstructionController { @repository(StepRepository) public stepRepository: StepRepository, @repository(UserRepository) protected progressRepository: ProgressRepository, - ) {} + ) { } @authenticate('jwt') @post('/users/{id}/instructions/{instructionId}', { @@ -466,6 +466,68 @@ export class UserInstructionController { return true; } + @authenticate('jwt') + @patch('/authorizate-for-premium-instruction/{instructionId}/{userId}', { + responses: { + '200': { + description: 'Authorize user for premium instructions', + content: { + 'application/json': { + schema: { + type: 'boolean', + }, + }, + }, + }, + }, + }) + async authorizeUserToPremiumInstruction( + @requestBody({ + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + key: {type: 'string'}, + }, + required: ['key'], + }, + }, + }, + }) + request: { + key: string; + }, + @param.query.number('instructionId') instructionId: number, + @param.query.number('userId') userId: number, + ): Promise { + const user = await this.userRepository.findById(this.user.id); + if (!user) { + throw new HttpErrors.NotFound('User not found'); + } + const instructionKey = + process.env.INSTRUCTION_KEY_PREMIUM_PERMISSIONS ?? ''; + const keyMatch = await this.hasher.comparePassword( + request.key, + instructionKey, + ); + if (!keyMatch) { + throw new HttpErrors.Unauthorized('Invalid password'); + } + const instruction = + await this.instructionRepository.findById(instructionId); + instruction.premiumUserIds = instruction.premiumUserIds ?? []; + if (instruction.premiumUserIds.includes(userId)) { + instruction.premiumUserIds = instruction.premiumUserIds.filter( + id => id !== userId, + ); + } else { + instruction.premiumUserIds.push(userId); + } + await this.instructionRepository.updateById(instructionId, instruction); + return true; + } + @get('/public-instructions', { responses: { '200': { @@ -630,63 +692,6 @@ export class UserInstructionController { return data; } - @patch('/authorizate-for-premium-instruction/{instructionId}/{userId}', { - responses: { - '200': { - description: 'Authorize user for premium instructions', - content: { - 'application/json': { - schema: { - type: 'boolean', - }, - }, - }, - }, - }, - }) - async authorizeUserToPremiumInstruction( - @requestBody({ - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - key: {type: 'string'}, - }, - required: ['key'], - }, - }, - }, - }) - request: { - key: string; - }, - @param.query.number('instructionId') instructionId: number, - @param.query.number('userId') userId: number, - ): Promise { - const instructionKey = - process.env.INSTRUCTION_KEY_PREMIUM_PERMISSIONS ?? ''; - const keyMatch = await this.hasher.comparePassword( - request.key, - instructionKey, - ); - if (!keyMatch) { - throw new HttpErrors.Unauthorized('Invalid password'); - } - const instruction = - await this.instructionRepository.findById(instructionId); - instruction.premiumUserIds = instruction.premiumUserIds ?? []; - if (instruction.premiumUserIds.includes(userId)) { - instruction.premiumUserIds = instruction.premiumUserIds.filter( - id => id !== userId, - ); - } else { - instruction.premiumUserIds.push(userId); - } - await this.instructionRepository.updateById(instructionId, instruction); - return true; - } - private validateInstructionOwnership(instruction: Instruction): void { if (Number(instruction.userId) !== Number(this.user.id)) { throw new HttpErrors.Forbidden( diff --git a/src/controllers/user-link.controller.ts b/src/controllers/user-link.controller.ts index 8f69333..2c3bab9 100644 --- a/src/controllers/user-link.controller.ts +++ b/src/controllers/user-link.controller.ts @@ -16,10 +16,23 @@ export class UserLinkController { @repository(UserRepository) public userRepository: UserRepository, @repository(UserLinkRepository) public userLinkRepository: UserLinkRepository, - ) {} + ) { } @authenticate('jwt') - @post('/users/{id}/follow/{followeeId}') + @post('/users/{id}/follow/{followeeId}', { + responses: { + '200': { + description: 'Follow User', + content: { + 'application/json': { + schema: { + type: 'boolean', + }, + }, + }, + }, + }, + }) async followUser( @param.path.number('followeeId') followeeId: number, ): Promise { @@ -39,7 +52,20 @@ export class UserLinkController { } @authenticate('jwt') - @del('/users/{id}/unfollow/{followeeId}') + @del('/users/{id}/unfollow/{followeeId}', { + responses: { + '200': { + description: 'Unfollow User', + content: { + 'application/json': { + schema: { + type: 'boolean', + }, + }, + }, + }, + }, + }) async unfollowUser( @param.path.number('followeeId') followeeId: number, ): Promise { @@ -63,12 +89,30 @@ export class UserLinkController { return true; } - @get('/users/{id}/followers') + @get('/users/{userId}/followers', { + responses: { + '200': { + description: 'Get followers', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + id: {type: 'number'}, + username: {type: 'string'}, + link: {type: 'string'}, + }, + }, + }, + }, + }, + }, + }) async getFollowers( - @param.path.number('id') userId: number, + @param.path.number('userId') userId: number, @param.query.number('limit') limit: number = 10, @param.query.number('offset') offset: number = 0, - ): Promise { + ): Promise> { const userLinks = await this.userLinkRepository.find({ where: { followeeId: userId, @@ -81,16 +125,49 @@ export class UserLinkController { where: { id: {inq: followerIds}, }, + fields: { + email: false, + passwordHash: false, + wrappedDEK: false, + initializationVector: false, + kekSalt: false, + language: false, + darkmode: false, + emailVerified: false, + date: false, + nick: false, + bio: false, + deleteHash: false, + favorites: false, + }, }); return followers; } - @get('/users/{id}/following') - async getFollowing( - @param.path.number('id') userId: number, + @get('/users/{userId}/followees', { + responses: { + '200': { + description: 'Get followees', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + id: {type: 'number'}, + username: {type: 'string'}, + link: {type: 'string'}, + }, + }, + }, + }, + }, + }, + }) + async getFollowees( + @param.path.number('userId') userId: number, @param.query.number('limit') limit: number = 10, @param.query.number('offset') offset: number = 0, - ): Promise { + ): Promise> { const userLinks = await this.userLinkRepository.find({ where: { followerId: userId, @@ -103,6 +180,21 @@ export class UserLinkController { where: { id: {inq: followeeIds}, }, + fields: { + email: false, + passwordHash: false, + wrappedDEK: false, + initializationVector: false, + kekSalt: false, + language: false, + darkmode: false, + emailVerified: false, + date: false, + nick: false, + bio: false, + deleteHash: false, + favorites: false, + }, }); return following; } diff --git a/src/controllers/user-progress.controller.ts b/src/controllers/user-progress.controller.ts index 64503b5..06e99d8 100644 --- a/src/controllers/user-progress.controller.ts +++ b/src/controllers/user-progress.controller.ts @@ -1,5 +1,8 @@ import {authenticate} from '@loopback/authentication'; -import {inject} from '@loopback/core'; +import { + JWTService +} from '@loopback/authentication-jwt'; +import {inject} from '@loopback/context'; import {repository} from '@loopback/repository'; import { HttpErrors, @@ -8,16 +11,15 @@ import { param, patch, post, - requestBody, + requestBody } from '@loopback/rest'; import {SecurityBindings, UserProfile} from '@loopback/security'; import {Progress, ProgressRelations} from '../models'; import { InstructionRepository, ProgressRepository, - UserRepository, + UserRepository } from '../repositories'; -import {JWTService} from '../services'; export class UserProgressController { constructor( @@ -176,7 +178,7 @@ export class UserProgressController { } @authenticate('jwt') - @get('/users/{id}/progress/{instructionId}', { + @get('/users/{id}/progresses/{instructionId}', { responses: { '200': { description: 'Progress model instance', diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 0489c95..d3a1edb 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -853,7 +853,7 @@ export class UserController { return usernamesAndLinks; } - @get('/user-detail/{id}', { + @get('/user-detail/{userId}', { responses: { '200': { description: 'Get user detail', @@ -910,7 +910,7 @@ export class UserController { }, }, }) - async getUserDetail(@param.query.number('id') userId: number): Promise<{ + async getUserDetail(@param.query.number('userId') userId: number): Promise<{ user: Omit< User, | 'email' diff --git a/src/services/jwt-service.ts b/src/services/jwt-service.ts index c1b9ee8..b7f53e0 100644 --- a/src/services/jwt-service.ts +++ b/src/services/jwt-service.ts @@ -8,7 +8,7 @@ import {TokenObject} from '@loopback/authentication-jwt'; import {inject} from '@loopback/core'; import {repository} from '@loopback/repository'; import {HttpErrors} from '@loopback/rest'; -import {securityId, UserProfile} from '@loopback/security'; +import {UserProfile, securityId} from '@loopback/security'; import {promisify} from 'util'; import { UserRepository @@ -27,8 +27,6 @@ export class JWTService implements TokenService { private jwtExpiresIn: string, @inject('services.user.service') public userService: MyUserService, - @inject('services.jwt.service') - public jwtService: JWTService, @repository(UserRepository) protected userRepository: UserRepository, ) { } @@ -96,7 +94,7 @@ export class JWTService implements TokenService { const userProfile: UserProfile = this.userService.convertToUserProfile(user); // create a JSON Web Token based on the user profile - const token = await this.jwtService.generateToken(userProfile); + const token = await this.generateToken(userProfile); return { accessToken: token, }; From 7d89bc1256063ecd597d85c7ad7da87769f98d72 Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Fri, 22 Sep 2023 16:30:22 +0200 Subject: [PATCH 14/15] fixes --- .../instruction-step.controller.ts | 28 ++++++------- .../user-instruction.controller.ts | 41 ++++++++++++------- src/controllers/user-progress.controller.ts | 10 ++--- src/controllers/user.controller.ts | 29 ++++--------- 4 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/controllers/instruction-step.controller.ts b/src/controllers/instruction-step.controller.ts index 19d7c0a..8afe5c5 100644 --- a/src/controllers/instruction-step.controller.ts +++ b/src/controllers/instruction-step.controller.ts @@ -31,7 +31,7 @@ export class InstructionStepController { @repository(InstructionRepository) public instructionRepository: InstructionRepository, @repository(StepRepository) public stepRepository: StepRepository, - ) {} + ) { } @authenticate('jwt') @post('/users/{id}/instructions/{instructionId}/steps/{stepId}', { @@ -48,8 +48,8 @@ export class InstructionStepController { }, }, }) - async create( - @param.path.number('id') instructionId: number, + async createStep( + @param.path.number('instructionId') instructionId: number, @requestBody({ content: { 'application/json': { @@ -76,7 +76,7 @@ export class InstructionStepController { }, }, }) - step: Omit, + step: Omit, ): Promise { const user = await this.userRepository.findById(this.user.id); if (!user) { @@ -110,9 +110,9 @@ export class InstructionStepController { }, }, }) - async patch( - @param.path.number('stepId') stepId: number, + async patchStep( @param.path.number('instructionId') instructionId: number, + @param.path.number('stepId') stepId: number, @requestBody({ content: { 'application/json': { @@ -160,7 +160,7 @@ export class InstructionStepController { } @authenticate('jwt') - @del('/users/{id}/instructions/{instructionId}/steps/{stepId}', { + @del('/users/{id}/instructions/{instructionId}/steps/${stepId}', { responses: { '200': { description: 'Delete Step', @@ -174,9 +174,9 @@ export class InstructionStepController { }, }, }) - async delete( - @param.path.number('stepId') stepId: number, + async deleteStep( @param.path.number('instructionId') instructionId: number, + @param.path.number('stepId') stepId: number, ): Promise { const user = await this.userRepository.findById(this.user.id); if (!user) { @@ -239,7 +239,7 @@ export class InstructionStepController { }, }, }) - async getPublicInstructions( + async getSteps( @param.path.number('instructionId') instructionId: number, ): Promise[]> { const user = await this.userRepository.findById(this.user.id); @@ -274,9 +274,9 @@ export class InstructionStepController { }, }, }) - async uploadPicture( - @param.path.number('stepId') stepId: number, + async uploadStepsPicture( @param.path.number('instructionId') instructionId: number, + @param.path.number('stepId') stepId: number, @requestBody({ content: { 'multipart/form-data': { @@ -335,9 +335,9 @@ export class InstructionStepController { }, }, }) - async deleteImage( - @param.path.number('stepId') stepId: number, + async deleteStepPicture( @param.path.number('instructionId') instructionId: number, + @param.path.number('stepId') stepId: number, ): Promise { const user = await this.userRepository.findById(this.user.id); if (!user) { diff --git a/src/controllers/user-instruction.controller.ts b/src/controllers/user-instruction.controller.ts index 87ae7f8..ae4f29e 100644 --- a/src/controllers/user-instruction.controller.ts +++ b/src/controllers/user-instruction.controller.ts @@ -58,7 +58,7 @@ export class UserInstructionController { }, }, }) - async create( + async createInstruction( @requestBody({ content: { 'application/json': { @@ -124,7 +124,7 @@ export class UserInstructionController { }, }, }) - async patch( + async patchInstruction( @param.path.number('instructionId') instructionId: number, @requestBody({ content: { @@ -181,8 +181,8 @@ export class UserInstructionController { }, }, }) - async delete( - @param.query.number('instructionId') instructionId: number, + async deleteInstruction( + @param.path.number('instructionId') instructionId: number, ): Promise { const userOriginal = await this.userRepository.findById(this.user.id); if (!userOriginal) { @@ -280,14 +280,14 @@ export class UserInstructionController { }, }) async getUsersInstructions(): Promise<{ - instreuctions: Omit[]; + instructions: Omit[]; progress: (Progress & ProgressRelations)[]; }> { const user = await this.userRepository.findById(this.user.id); if (!user) { throw new HttpErrors.NotFound('User not found'); } - const instreuctions = await this.instructionRepository.find({ + const instructions = await this.instructionRepository.find({ where: { userId: this.user.id, }, @@ -311,7 +311,7 @@ export class UserInstructionController { userId: this.user.id, }, }); - return {instreuctions, progress}; + return {instructions, progress}; } @authenticate('jwt') @@ -329,7 +329,7 @@ export class UserInstructionController { }, }, }) - async uploadPicture( + async uploadInstructionPicture( @param.path.number('instructionId') instructionId: number, @requestBody({ content: { @@ -384,7 +384,7 @@ export class UserInstructionController { }, }, }) - async deleteImage( + async deleteInstructionPicture( @param.path.number('instructionId') instructionId: number, ): Promise { const user = await this.userRepository.findById(this.user.id); @@ -423,12 +423,25 @@ export class UserInstructionController { }, }, }) - async setPremium( + async setPremiumInstruction( @param.path.number('instructionId') instructionId: number, - @requestBody() + @requestBody({ + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + key: {type: 'string'}, + premium: {type: 'boolean'}, + }, + required: ['key', 'premium'], + }, + }, + }, + }) request: { - premium: boolean; key: string; + premium: boolean; }, ): Promise { const user = await this.userRepository.findById(this.user.id); @@ -482,6 +495,8 @@ export class UserInstructionController { }, }) async authorizeUserToPremiumInstruction( + @param.path.number('instructionId') instructionId: number, + @param.path.number('userId') userId: number, @requestBody({ content: { 'application/json': { @@ -498,8 +513,6 @@ export class UserInstructionController { request: { key: string; }, - @param.query.number('instructionId') instructionId: number, - @param.query.number('userId') userId: number, ): Promise { const user = await this.userRepository.findById(this.user.id); if (!user) { diff --git a/src/controllers/user-progress.controller.ts b/src/controllers/user-progress.controller.ts index 06e99d8..335a6a1 100644 --- a/src/controllers/user-progress.controller.ts +++ b/src/controllers/user-progress.controller.ts @@ -49,7 +49,7 @@ export class UserProgressController { }, }, }) - async create( + async createProgress( @requestBody({ content: { 'application/json': { @@ -105,7 +105,7 @@ export class UserProgressController { }, }, }) - async patch( + async patchProgress( @param.path.number('instructionId') instructionId: number, @requestBody({ content: { @@ -156,8 +156,8 @@ export class UserProgressController { }, }, }) - async delete( - @param.query.number('instructionId') instructionId: number, + async deleteProgress( + @param.path.number('instructionId') instructionId: number, ): Promise { const userOriginal = await this.userRepository.findById(this.user.id); if (!userOriginal) { @@ -199,7 +199,7 @@ export class UserProgressController { }, }, }) - async find( + async getProgress( @param.path.number('instructionId') instructionId: number, ): Promise { const user = await this.userRepository.findById(this.user.id); diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index d3a1edb..a4c6d4c 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -623,7 +623,7 @@ export class UserController { } @authenticate('jwt') - @del('/users/{id}', { + @del('/users/{id}/{password}', { responses: { '200': { description: 'Delete user', @@ -638,29 +638,14 @@ export class UserController { }, }) async deleteUser( - @requestBody({ - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - password: {type: 'string'}, - }, - required: ['password'], - }, - }, - }, - }) - request: { - password: string; - }, + @param.path.password('password') password: string, ): Promise { const userOriginal = await this.userRepository.findById(this.user.id); if (!userOriginal) { throw new HttpErrors.NotFound('User not found'); } const passwordMatched = await this.hasher.comparePassword( - request.password, + password, userOriginal.passwordHash, ); if (!passwordMatched) { @@ -910,7 +895,9 @@ export class UserController { }, }, }) - async getUserDetail(@param.query.number('userId') userId: number): Promise<{ + async getUserDetail( + @param.path.number('userId') userId: number + ): Promise<{ user: Omit< User, | 'email' @@ -927,8 +914,8 @@ export class UserController { >; followerCount: number; followeeCount: number; - instructions: Omit[]; - instructionsPremium: Omit[]; + instructions: Omit[] | null; + instructionsPremium: Omit[] | null; }> { const user = await this.userRepository.findById(userId, { fields: { From 950c4ee9f67aaa68b41f0adaadf18376bb23c972 Mon Sep 17 00:00:00 2001 From: Szotkowski Date: Fri, 22 Sep 2023 16:55:21 +0200 Subject: [PATCH 15/15] fix policy --- src/services/example-user-policy.hcl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/example-user-policy.hcl b/src/services/example-user-policy.hcl index 3db9576..9de5197 100644 --- a/src/services/example-user-policy.hcl +++ b/src/services/example-user-policy.hcl @@ -7,6 +7,6 @@ path "transit/decrypt/{{id}}/*" { path "auth/token/renew-self" { capabilities = ["update"] } -path "auth/token/create" { +path "auth/userpass/login/*" { capabilities = ["create"] }