diff --git a/back/prisma/migrations/20230227220027_/migration.sql b/back/prisma/migrations/20230227220027_/migration.sql new file mode 100644 index 0000000..e37a741 --- /dev/null +++ b/back/prisma/migrations/20230227220027_/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "UserSettings" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "pushNotification" BOOLEAN NOT NULL DEFAULT true, + "emailNotification" BOOLEAN NOT NULL DEFAULT true, + "trainingNotification" BOOLEAN NOT NULL DEFAULT true, + "newsongNotification" BOOLEAN NOT NULL DEFAULT true, + "dataCollection" BOOLEAN NOT NULL DEFAULT true, + "CustomAdds" BOOLEAN NOT NULL DEFAULT true, + "Recommendations" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "UserSettings_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserSettings_userId_key" ON "UserSettings"("userId"); + +-- AddForeignKey +ALTER TABLE "UserSettings" ADD CONSTRAINT "UserSettings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/back/prisma/migrations/20230328185436_/migration.sql b/back/prisma/migrations/20230328185436_/migration.sql new file mode 100644 index 0000000..99c5f59 --- /dev/null +++ b/back/prisma/migrations/20230328185436_/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - You are about to drop the column `CustomAdds` on the `UserSettings` table. All the data in the column will be lost. + - You are about to drop the column `Recommendations` on the `UserSettings` table. All the data in the column will be lost. + - You are about to drop the column `dataCollection` on the `UserSettings` table. All the data in the column will be lost. + - You are about to drop the column `newsongNotification` on the `UserSettings` table. All the data in the column will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "UserSettings" DROP CONSTRAINT "UserSettings_userId_fkey"; + +-- AlterTable +ALTER TABLE "UserSettings" DROP COLUMN "CustomAdds", +DROP COLUMN "Recommendations", +DROP COLUMN "dataCollection", +DROP COLUMN "newsongNotification", +ADD COLUMN "leaderBoard" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "newSongNotification" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "recommendations" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "showActivity" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "weeklyReport" BOOLEAN NOT NULL DEFAULT true; + +-- AddForeignKey +ALTER TABLE "UserSettings" ADD CONSTRAINT "UserSettings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/back/prisma/schema.prisma b/back/prisma/schema.prisma index 77708f4..81a6526 100644 --- a/back/prisma/schema.prisma +++ b/back/prisma/schema.prisma @@ -10,15 +10,30 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - username String @unique - password String - email String - isGuest Boolean @default(false) - partyPlayed Int @default(0) - LessonHistory LessonHistory[] - SongHistory SongHistory[] - searchHistory SearchHistory[] + id Int @id @default(autoincrement()) + username String @unique + password String + email String + isGuest Boolean @default(false) + partyPlayed Int @default(0) + LessonHistory LessonHistory[] + SongHistory SongHistory[] + searchHistory SearchHistory[] + settings UserSettings? +} + +model UserSettings { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int @unique + pushNotification Boolean @default(true) + emailNotification Boolean @default(true) + trainingNotification Boolean @default(true) + newSongNotification Boolean @default(true) + recommendations Boolean @default(true) + weeklyReport Boolean @default(true) + leaderBoard Boolean @default(true) + showActivity Boolean @default(true) } model SearchHistory { diff --git a/back/src/app.module.ts b/back/src/app.module.ts index 5f98408..8142e0d 100644 --- a/back/src/app.module.ts +++ b/back/src/app.module.ts @@ -7,13 +7,11 @@ import { PrismaModule } from './prisma/prisma.module'; import { AuthModule } from './auth/auth.module'; import { SongModule } from './song/song.module'; import { LessonModule } from './lesson/lesson.module'; -import { ArtistController } from './artist/artist.controller'; +import { SettingsModule } from './settings/settings.module'; import { ArtistService } from './artist/artist.service'; import { GenreModule } from './genre/genre.module'; import { ArtistModule } from './artist/artist.module'; import { AlbumModule } from './album/album.module'; -import { SearchController } from './search/search.controller'; -import { SearchService } from './search/search.service'; import { SearchModule } from './search/search.module'; import { HistoryModule } from './history/history.module'; @@ -28,6 +26,7 @@ import { HistoryModule } from './history/history.module'; ArtistModule, AlbumModule, SearchModule, + SettingsModule, HistoryModule, ], controllers: [AppController], diff --git a/back/src/auth/auth.controller.ts b/back/src/auth/auth.controller.ts index 35af1f1..710d980 100644 --- a/back/src/auth/auth.controller.ts +++ b/back/src/auth/auth.controller.ts @@ -10,6 +10,8 @@ import { HttpCode, Put, InternalServerErrorException, + Patch, + NotFoundException, } from '@nestjs/common'; import { AuthService } from './auth.service'; import { JwtAuthGuard } from './jwt-auth.guard'; @@ -28,6 +30,9 @@ import { User } from '../models/user'; import { JwtToken } from './models/jwt'; import { LoginDto } from './dto/login.dto'; import { Profile } from './dto/profile.dto'; +import { Setting } from 'src/models/setting'; +import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto'; +import { SettingsService } from 'src/settings/settings.service'; @ApiTags('auth') @Controller('auth') @@ -35,12 +40,15 @@ export class AuthController { constructor( private authService: AuthService, private usersService: UsersService, + private settingsService: SettingsService, ) {} @Post('register') async register(@Body() registerDto: RegisterDto): Promise { try { - await this.usersService.createUser(registerDto); + await this.usersService.createUser(registerDto).then((user) => { + this.settingsService.createUserSetting(user.id); + }); } catch { throw new BadRequestException(); } @@ -105,4 +113,29 @@ export class AuthController { deleteSelf(@Request() req: any): Promise { return this.usersService.deleteUser({ id: req.user.id }); } + + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({description: 'Successfully edited settings', type: Setting}) + @ApiUnauthorizedResponse({description: 'Invalid token'}) + @Patch('me/settings') + udpateSettings( + @Request() req: any, + @Body() settingUserDto: UpdateSettingDto): Promise { + return this.settingsService.updateUserSettings({ + where: { userId: +req.user.id}, + data: settingUserDto, + }); + } + + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse({description: 'Successfully edited settings', type: Setting}) + @ApiUnauthorizedResponse({description: 'Invalid token'}) + @Get('me/settings') + async getSettings(@Request() req: any): Promise { + const result = await this.settingsService.getUserSetting({ userId: +req.user.id }); + if (!result) throw new NotFoundException(); + return result; + } } diff --git a/back/src/auth/auth.module.ts b/back/src/auth/auth.module.ts index 3f09593..7ac9c82 100644 --- a/back/src/auth/auth.module.ts +++ b/back/src/auth/auth.module.ts @@ -8,11 +8,13 @@ import { JwtModule } from '@nestjs/jwt'; import { ConfigModule } from '@nestjs/config'; import { ConfigService } from '@nestjs/config'; import { JwtStrategy } from './jwt.strategy'; +import { SettingsModule } from 'src/settings/settings.module'; @Module({ imports: [ ConfigModule, UsersModule, + SettingsModule, PassportModule, JwtModule.registerAsync({ imports: [ConfigModule], diff --git a/back/src/models/setting.ts b/back/src/models/setting.ts new file mode 100644 index 0000000..6af8db7 --- /dev/null +++ b/back/src/models/setting.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class Setting { + @ApiProperty() + id: number; + @ApiProperty() + userId: number; + @ApiProperty() + pushNotification: boolean; + @ApiProperty() + emailNotification: boolean; + @ApiProperty() + trainingNotification: boolean; + @ApiProperty() + newSongNotification: boolean; + @ApiProperty() + recommendations: boolean; + @ApiProperty() + weeklyReport: boolean; + @ApiProperty() + leaderBoard: boolean; + @ApiProperty() + showActivity: boolean; +} diff --git a/back/src/settings/dto/update-setting.dto.ts b/back/src/settings/dto/update-setting.dto.ts new file mode 100644 index 0000000..e033831 --- /dev/null +++ b/back/src/settings/dto/update-setting.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateSettingDto { + @ApiProperty() + pushNotification?: boolean; + @ApiProperty() + emailNotification?: boolean; + @ApiProperty() + trainingNotification?: boolean; + @ApiProperty() + newSongNotification?: boolean; + @ApiProperty() + recommendations?: boolean; + @ApiProperty() + weeklyReport?: boolean; + @ApiProperty() + leaderBoard?: boolean; + @ApiProperty() + showActivity?: boolean; +} diff --git a/back/src/settings/settings.controller.spec.ts b/back/src/settings/settings.controller.spec.ts new file mode 100644 index 0000000..2e6067c --- /dev/null +++ b/back/src/settings/settings.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SettingsController } from './settings.controller'; + +describe('SettingsController', () => { + let controller: SettingsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SettingsController], + }).compile(); + + controller = module.get(SettingsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/back/src/settings/settings.module.ts b/back/src/settings/settings.module.ts new file mode 100644 index 0000000..bd3000f --- /dev/null +++ b/back/src/settings/settings.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SettingsService } from './settings.service'; +import { PrismaModule } from 'src/prisma/prisma.module'; + +@Module({ + imports: [PrismaModule], + providers: [SettingsService], + exports: [SettingsService], +}) +export class SettingsModule {} diff --git a/back/src/settings/settings.service.spec.ts b/back/src/settings/settings.service.spec.ts new file mode 100644 index 0000000..9001518 --- /dev/null +++ b/back/src/settings/settings.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SettingsService } from './settings.service'; + +describe('SettingsService', () => { + let service: SettingsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SettingsService], + }).compile(); + + service = module.get(SettingsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/back/src/settings/settings.service.ts b/back/src/settings/settings.service.ts new file mode 100644 index 0000000..cda9072 --- /dev/null +++ b/back/src/settings/settings.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma, UserSettings } from '@prisma/client'; +import { PrismaService } from 'src/prisma/prisma.service'; + +@Injectable() +export class SettingsService { + constructor(private prisma: PrismaService) {} + + async getUserSetting( + settingWhereUniqueInput: Prisma.UserSettingsWhereUniqueInput, + ): Promise { + return this.prisma.userSettings.findUnique({ + where: settingWhereUniqueInput, + }); + } + + async createUserSetting(userId: number): Promise { + return this.prisma.userSettings.create({ + data: { + user: { + connect: { + id: userId, + } + } + } + }) + } + + async updateUserSettings(params: { + where: Prisma.UserSettingsWhereUniqueInput; + data: Prisma.UserSettingsUpdateInput; + }): Promise { + const { where, data } = params; + return this.prisma.userSettings.update({ + data, + where, + }); + } + + async deleteUserSettings(where: Prisma.UserSettingsWhereUniqueInput): Promise { + return this.prisma.userSettings.delete({ + where, + }); + } +} diff --git a/back/src/users/users.controller.ts b/back/src/users/users.controller.ts index c505546..f049e46 100644 --- a/back/src/users/users.controller.ts +++ b/back/src/users/users.controller.ts @@ -8,18 +8,23 @@ import { NotFoundException, } from '@nestjs/common'; import { UsersService } from './users.service'; +import { SettingsService } from 'src/settings/settings.service'; import { CreateUserDto } from './dto/create-user.dto'; import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger'; import { User } from 'src/models/user'; +import { resolve } from 'path'; @ApiTags('users') @Controller('users') export class UsersController { - constructor(private readonly usersService: UsersService) {} + constructor(private readonly usersService: UsersService, private readonly settingsService: SettingsService) {} @Post() create(@Body() createUserDto: CreateUserDto): Promise { - return this.usersService.createUser(createUserDto); + return this.usersService.createUser(createUserDto).then((user) => { + this.settingsService.createUserSetting(user.id); + return user; + }).catch((e) => e); } @Get() diff --git a/back/src/users/users.module.ts b/back/src/users/users.module.ts index d1e76e7..e117ce3 100644 --- a/back/src/users/users.module.ts +++ b/back/src/users/users.module.ts @@ -2,11 +2,12 @@ import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; import { PrismaModule } from 'src/prisma/prisma.module'; +import { SettingsService } from 'src/settings/settings.service'; @Module({ imports: [PrismaModule], controllers: [UsersController], - providers: [UsersService], + providers: [UsersService, SettingsService], exports: [UsersService], }) export class UsersModule {} diff --git a/back/test/robot/settings/settings.robot b/back/test/robot/settings/settings.robot new file mode 100644 index 0000000..1c436b6 --- /dev/null +++ b/back/test/robot/settings/settings.robot @@ -0,0 +1,27 @@ +*** Settings *** +Documentation Tests of the /settings route. +... Ensures that the settings CRUD works corectly as well as the automation with the user creation. + +Resource ../rest.resource +Resource ../auth/auth.resource + + +*** Test Cases *** +Get settings + [Documentation] Create a user and get associated settings + ${userID}= RegisterLogin 2na-min-faranssa-wa-2na-adrus-allu3'at-al3rabia + &{get}= GET /auth/me/settings/ + Output + Should Be True ${get.body.emailNotification} + Integer response status 200 + [Teardown] DELETE /users/${userID} + +Patch settingspushNotification + ${userID}= RegisterLogin 2na-min-faranssa-wa-2na-adrus-allu3'at-al3rabia + &{patch}= PATCH + ... /auth/me/settings/ + ... {"pushNotification": true, "emailNotification": true, "trainingNotification": true, "newSongNotification": true, "recommendations": true, "weeklyReport": true, "leaderBoard": false, "showActivity": true} + Output + Should Not Be True ${patch.body.leaderBoard} + Integer response status 200 + [Teardown] DELETE /users/${userID}