Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding the users' crud endpoints #19

Merged
merged 9 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions apps/backend/.env.example

This file was deleted.

2 changes: 2 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.3.8",
"@prisma/client": "^5.13.0",
"class-transformer": "^0.5.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lmao why was this missing

"class-validator": "^0.14.1",
"nestjs-prisma": "^0.23.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
Expand Down
90 changes: 90 additions & 0 deletions apps/backend/prisma/migrations/20240811135842_dev/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('BODY_ADMIN', 'BODY_MEMBER', 'USER');

-- CreateEnum
CREATE TYPE "ApplicationStatus" AS ENUM ('SUBMITTED', 'ACCEPTED', 'REJECTED', 'NEEDS_REVIEW', 'FINISHED');

-- CreateTable
CREATE TABLE "User" (
"authSchId" TEXT NOT NULL,
"fullName" TEXT NOT NULL,
"nickName" TEXT NOT NULL,
"role" "Role" NOT NULL DEFAULT 'USER',
"neptun" TEXT,
"email" TEXT,
"isSchResident" BOOLEAN NOT NULL,
"isActiveVikStudent" BOOLEAN NOT NULL,
"roomNumber" INTEGER,
"profileImage" BYTEA,
"canHelpNoobs" BOOLEAN NOT NULL DEFAULT false,
"publicDesc" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"profileSeenAt" TIMESTAMP(3),

CONSTRAINT "User_pkey" PRIMARY KEY ("authSchId")
);

-- CreateTable
CREATE TABLE "ApplicationPeriod" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"applicationPeriodStartAt" TIMESTAMP(3) NOT NULL,
"applicationPeriodEndAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"ticketsAreValid" BOOLEAN NOT NULL DEFAULT false,
"authorId" TEXT NOT NULL,

CONSTRAINT "ApplicationPeriod_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Application" (
"id" SERIAL NOT NULL,
"userId" TEXT NOT NULL,
"applicationPeriodId" INTEGER NOT NULL,
"status" "ApplicationStatus" NOT NULL DEFAULT 'SUBMITTED',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "Application_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Post" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"preview" TEXT NOT NULL,
"visible" BOOLEAN NOT NULL DEFAULT true,
"authorId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_neptun_key" ON "User"("neptun");

-- CreateIndex
CREATE INDEX "ApplicationPeriod_authorId_idx" ON "ApplicationPeriod"("authorId");

-- CreateIndex
CREATE UNIQUE INDEX "Application_userId_applicationPeriodId_key" ON "Application"("userId", "applicationPeriodId");

-- CreateIndex
CREATE INDEX "Post_authorId_idx" ON "Post"("authorId");

-- AddForeignKey
ALTER TABLE "ApplicationPeriod" ADD CONSTRAINT "ApplicationPeriod_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("authSchId") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Application" ADD CONSTRAINT "Application_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("authSchId") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Application" ADD CONSTRAINT "Application_applicationPeriodId_fkey" FOREIGN KEY ("applicationPeriodId") REFERENCES "ApplicationPeriod"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("authSchId") ON DELETE RESTRICT ON UPDATE CASCADE;
3 changes: 3 additions & 0 deletions apps/backend/prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
7 changes: 7 additions & 0 deletions apps/backend/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
})
);
app.enableCors({
origin: [process.env.FRONTEND_URL],
methods: ['GET', 'POST', 'PATCH', 'DELETE', 'PUT'],
Expand Down
27 changes: 27 additions & 0 deletions apps/backend/src/user/dto/update-user-admin.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { IsBoolean, IsEmail, IsNumber, IsOptional, IsString } from 'class-validator';

export class UpdateUserAdminDto {
@IsString()
@IsOptional()
nickName: string;

@IsBoolean()
@IsOptional()
isSchResident: boolean;

@IsNumber()
@IsOptional()
roomNumber: number;

@IsEmail()
@IsOptional()
email: string;

@IsBoolean()
@IsOptional()
canHelpNoobs: boolean;

@IsString()
@IsOptional()
publicDesc: string;
}
19 changes: 19 additions & 0 deletions apps/backend/src/user/dto/update-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IsBoolean, IsEmail, IsNumber, IsOptional, IsString } from 'class-validator';

export class UpdateUserDto {
@IsString()
@IsOptional()
nickName: string;

@IsBoolean()
@IsOptional()
isSchResident: boolean;

@IsNumber()
@IsOptional()
roomNumber: number;

@IsEmail()
@IsOptional()
email: string;
}
19 changes: 19 additions & 0 deletions apps/backend/src/user/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Role } from '@prisma/client';

export class UserEntity {
authSchId: string;
fullName: string;
nickName: string;
role: Role;
neptun?: string;
email?: string;
isSchResident: boolean;
isActiveVikStudent: boolean;
roomNumber?: number;
profileImage?: Buffer;
canHelpNoobs: boolean;
publicDesc?: string;
createdAt: Date;
updatedAt: Date;
profileSeenAt?: Date;
}
45 changes: 43 additions & 2 deletions apps/backend/src/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
import { Controller } from '@nestjs/common';
import { CurrentUser } from '@kir-dev/passport-authsch';
import { Body, Controller, Get, Param, ParseIntPipe, Patch, Query, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Role } from '@prisma/client';

import { Roles } from '../auth/decorators/Roles.decorator';
import { RolesGuard } from '../auth/roles.guard';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserEntity } from './entities/user.entity';
import { UserService } from './user.service';

@Controller('user')
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}

@Get()
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(Role.BODY_ADMIN, Role.BODY_MEMBER)
async findAll(@Query('page', ParseIntPipe) page: number = 1, @Query('pageSize', ParseIntPipe) pageSize: number = 10) {
return this.userService.findMany(page, pageSize);
}

@Get('me')
@UseGuards(AuthGuard('jwt'))
async getCurrentUser(@CurrentUser() user: UserEntity) {
return user;
}

@Patch('me')
@UseGuards(AuthGuard('jwt'))
@Roles(Role.BODY_ADMIN, Role.BODY_MEMBER, Role.USER)
async updateCurrentUser(@Body() updateUserDto: UpdateUserDto, @CurrentUser() user: UserEntity) {
return this.userService.update(user.authSchId, updateUserDto);
}

@Get(':id')
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(Role.BODY_ADMIN, Role.BODY_MEMBER)
async findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}

@Patch(':id')
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(Role.BODY_ADMIN, Role.BODY_MEMBER)
async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to add the option for the user to update their own profile also. For eg nickname or profile description should be for sure editable. But on the other hand, schresident and vik student should be managed by admins.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But these are only for schbody members, right? No point in regular users having nicknames and stuff

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, that is fair. I forgot about that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so @Dkrisztan I think body members should be able to edit their own nickname and description
and body members and admins should be able to edit the schResident and vikStudent fields (if this is what they'll use during belépőosztás. or maybe I'm confused)

Copy link
Contributor Author

@Dkrisztan Dkrisztan Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first part makes sense, the second not really 😅 . So body members can edit nickname desc and resident and vikstudent fields as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are not able to check who lives in the dorm and who does not, we agreed on all users will be able to edit their email, sch-status (room number also) as some kind of a self-declaration. Or at least, it is what I remember.
Now the profile page also has a nickname field (for all users), it is very useful for the symetry of the UI :), do you think it is unneccessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. makes sense I just didn't understand the difference between the admin and member role, regarding this update. This fix will be up later this day.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the point of a nickname and description for regular users, their profile won't even be public, right? or are we planning on turning this app into a workout social network? xd

makes sense that the user can set their schStatus and roomNumber, but then the body members should be able to edit another flag that the sch residency was approved during the belepoosztas. Or I guess that could be unnecessary, they just simply don't give them their pass if it wasn't approved

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

return this.userService.update(id, updateUserDto);
}
}
63 changes: 61 additions & 2 deletions apps/backend/src/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,63 @@
import { Injectable } from '@nestjs/common';
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'nestjs-prisma';

import { UpdateUserDto } from './dto/update-user.dto';
import { UserEntity } from './entities/user.entity';

@Injectable()
export class UserService {}
export class UserService {
constructor(private readonly prisma: PrismaService) {}

async findOne(id: string): Promise<UserEntity> {
const user = this.prisma.user.findUnique({
where: { authSchId: id },
});

if (user === null) {
throw new NotFoundException(`User with id ${id} not found`);
}

return user;
}

async findMany(
page: number,
pageSize: number
): Promise<{ users: UserEntity[]; pageNumber: number; totalUsers: number }> {
const [totalUsers, users] = await Promise.all([
this.prisma.user.count(),
this.prisma.user.findMany({
orderBy: { fullName: 'asc' },
skip: (page - 1) * pageSize,
take: pageSize,
}),
]);

return {
users,
pageNumber: page,
totalUsers,
};
}

async update(id: string, updateUserDto: UpdateUserDto): Promise<UserEntity> {
const user = await this.prisma.user.findUnique({ where: { authSchId: id } });

if (user === null) {
throw new NotFoundException(`User with id ${id} not found`);
} else if (user.isSchResident === false && updateUserDto.roomNumber) {
throw new BadRequestException('Non-resident users cannot have a room number');
}

return this.prisma.user.update({ where: { authSchId: id }, data: updateUserDto });
}

// TODO maybe remove it? currently not used (could be useful later)
async delete(id: string): Promise<UserEntity> {
try {
return this.prisma.user.delete({ where: { authSchId: id } });
} catch (error) {
throw new NotFoundException(`User with id ${id} not found`);
}
}
}
Loading