Feature/adc/#50 users settings route (#89)
* #50 - migration 20221023123919_ + route settings * #50 - migration 20221023123919_ + route settings * #50 - settings creation at user creation + update migration * changed settings acces from by id to userId * deleting the user results in deleting it's associated userSettings row * pr fixes + robot tests + other minor fixes * removed useless comments * added settings endpoint to /auth/me and automated creation to /register * clean code before merge
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<void> {
|
||||
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<User> {
|
||||
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<Setting> {
|
||||
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<Setting> {
|
||||
const result = await this.settingsService.getUserSetting({ userId: +req.user.id });
|
||||
if (!result) throw new NotFoundException();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>(SettingsController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -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 {}
|
||||
@@ -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>(SettingsService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -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<UserSettings | null> {
|
||||
return this.prisma.userSettings.findUnique({
|
||||
where: settingWhereUniqueInput,
|
||||
});
|
||||
}
|
||||
|
||||
async createUserSetting(userId: number): Promise<UserSettings> {
|
||||
return this.prisma.userSettings.create({
|
||||
data: {
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async updateUserSettings(params: {
|
||||
where: Prisma.UserSettingsWhereUniqueInput;
|
||||
data: Prisma.UserSettingsUpdateInput;
|
||||
}): Promise<UserSettings> {
|
||||
const { where, data } = params;
|
||||
return this.prisma.userSettings.update({
|
||||
data,
|
||||
where,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUserSettings(where: Prisma.UserSettingsWhereUniqueInput): Promise<UserSettings> {
|
||||
return this.prisma.userSettings.delete({
|
||||
where,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<User> {
|
||||
return this.usersService.createUser(createUserDto);
|
||||
return this.usersService.createUser(createUserDto).then((user) => {
|
||||
this.settingsService.createUserSetting(user.id);
|
||||
return user;
|
||||
}).catch((e) => e);
|
||||
}
|
||||
|
||||
@Get()
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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}
|
||||
Reference in New Issue
Block a user