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 {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
username String @unique
|
username String @unique
|
||||||
password String
|
password String
|
||||||
email String
|
email String
|
||||||
isGuest Boolean @default(false)
|
isGuest Boolean @default(false)
|
||||||
partyPlayed Int @default(0)
|
partyPlayed Int @default(0)
|
||||||
LessonHistory LessonHistory[]
|
LessonHistory LessonHistory[]
|
||||||
SongHistory SongHistory[]
|
SongHistory SongHistory[]
|
||||||
searchHistory SearchHistory[]
|
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 {
|
model SearchHistory {
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import { PrismaModule } from './prisma/prisma.module';
|
|||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { SongModule } from './song/song.module';
|
import { SongModule } from './song/song.module';
|
||||||
import { LessonModule } from './lesson/lesson.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 { ArtistService } from './artist/artist.service';
|
||||||
import { GenreModule } from './genre/genre.module';
|
import { GenreModule } from './genre/genre.module';
|
||||||
import { ArtistModule } from './artist/artist.module';
|
import { ArtistModule } from './artist/artist.module';
|
||||||
import { AlbumModule } from './album/album.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 { SearchModule } from './search/search.module';
|
||||||
import { HistoryModule } from './history/history.module';
|
import { HistoryModule } from './history/history.module';
|
||||||
|
|
||||||
@@ -28,6 +26,7 @@ import { HistoryModule } from './history/history.module';
|
|||||||
ArtistModule,
|
ArtistModule,
|
||||||
AlbumModule,
|
AlbumModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
|
SettingsModule,
|
||||||
HistoryModule,
|
HistoryModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
HttpCode,
|
HttpCode,
|
||||||
Put,
|
Put,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
|
Patch,
|
||||||
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
@@ -28,6 +30,9 @@ import { User } from '../models/user';
|
|||||||
import { JwtToken } from './models/jwt';
|
import { JwtToken } from './models/jwt';
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from './dto/login.dto';
|
||||||
import { Profile } from './dto/profile.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')
|
@ApiTags('auth')
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
@@ -35,12 +40,15 @@ export class AuthController {
|
|||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private usersService: UsersService,
|
private usersService: UsersService,
|
||||||
|
private settingsService: SettingsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('register')
|
@Post('register')
|
||||||
async register(@Body() registerDto: RegisterDto): Promise<void> {
|
async register(@Body() registerDto: RegisterDto): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.usersService.createUser(registerDto);
|
await this.usersService.createUser(registerDto).then((user) => {
|
||||||
|
this.settingsService.createUserSetting(user.id);
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
throw new BadRequestException();
|
throw new BadRequestException();
|
||||||
}
|
}
|
||||||
@@ -105,4 +113,29 @@ export class AuthController {
|
|||||||
deleteSelf(@Request() req: any): Promise<User> {
|
deleteSelf(@Request() req: any): Promise<User> {
|
||||||
return this.usersService.deleteUser({ id: req.user.id });
|
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 { ConfigModule } from '@nestjs/config';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { JwtStrategy } from './jwt.strategy';
|
import { JwtStrategy } from './jwt.strategy';
|
||||||
|
import { SettingsModule } from 'src/settings/settings.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
SettingsModule,
|
||||||
PassportModule,
|
PassportModule,
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
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,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
|
import { SettingsService } from 'src/settings/settings.service';
|
||||||
import { CreateUserDto } from './dto/create-user.dto';
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { User } from 'src/models/user';
|
import { User } from 'src/models/user';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
@ApiTags('users')
|
@ApiTags('users')
|
||||||
@Controller('users')
|
@Controller('users')
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
constructor(private readonly usersService: UsersService) {}
|
constructor(private readonly usersService: UsersService, private readonly settingsService: SettingsService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() createUserDto: CreateUserDto): Promise<User> {
|
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()
|
@Get()
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
|
|||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
import { UsersController } from './users.controller';
|
import { UsersController } from './users.controller';
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
|
import { SettingsService } from 'src/settings/settings.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
controllers: [UsersController],
|
controllers: [UsersController],
|
||||||
providers: [UsersService],
|
providers: [UsersService, SettingsService],
|
||||||
exports: [UsersService],
|
exports: [UsersService],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
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