Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
44 changes: 44 additions & 0 deletions backend/src/controllers/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,3 +561,47 @@ export class TeamUpdateDTO {
@Expose()
public description!: string;
}

export class CriteriaDTO {
@Expose()
public readonly id!: number;
@Expose()
public title!: string;
@Expose()
public description!: string;
}

export class ProjectDTO {
@Expose()
public readonly id!: number;
@Expose()
@Type(() => TeamDTO)
@ValidateNested()
public team!: TeamDTO;
@Expose()
public title!: string;
@Expose()
public description!: string;
@Expose()
public allowRating!: boolean;
}

export class RatingDTO {
@Expose()
public readonly id!: number;
@Expose()
@Type(() => UserDTO)
@ValidateNested()
public user!: UserDTO;
@Expose()
@Type(() => ProjectDTO)
@ValidateNested()
public project!: ProjectDTO;
@Expose()
@Type(() => CriteriaDTO)
@ValidateNested()
public critera!: CriteriaDTO;
@Expose()
// 1 - 5
public rating!: number;
}
31 changes: 31 additions & 0 deletions backend/src/controllers/project-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Authorized, Delete, JsonController, NotFoundError } from "routing-controllers";
import { Inject } from "typedi";
import { UserRole } from "../entities/user-role";
import { IProjectService, ProjectServiceToken } from "../services/project-service";

// TODO for every team, add a new project automatically with the correct teamId

@JsonController("/projects")
export class ProjectController {
public constructor(
@Inject(ProjectServiceToken) private readonly _projects: IProjectService,
) {}

/**
* Update a project (mvp: create one project per team)
*/
@Put("/project/:id")
@Authorized(UserRole.User)
public async updateProject(
@Param("id") projectId: number,
@Body() { data: projectDTO }: { data: ProjectDTO },
): Promise<TeamDTO> {
// TODO ProjectUpdateDTO?
const project = convertBetweenEntityAndDTO(projectDTO, Project);

// TODO how to make actual not found errors for incorrect ids?

const updateProject = await this._ratings.updateProject(project, user);
return convertBetweenEntityAndDTO(updateProject, ProjectDTO);
}
}
98 changes: 98 additions & 0 deletions backend/src/controllers/rating-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Authorized, Delete, JsonController, ForbiddenError } from "routing-controllers";
import { Inject } from "typedi";
import { UserRole } from "../entities/user-role";
import { SettingsServiceToken } from "../services/settings-service";
import { SettingsServiceToken } from "../services/settings-service";

// The RatingController and RatingService group stuff concerning ratings and critiera
// together. Feel free to separate, if you think that would be better.

@JsonController("/ratings")
export class RatingController {
public constructor(
@Inject(SettingsServiceToken) private readonly _settings: ISettingsService,
@Inject(RatingServiceToken) private readonly _ratings: IRatingService,
) {}

/**
* Allow users to rate a specific project (if ratings are enabled in the application
* settings).
*
* By using the application setting, admins can prepare the projects that can be
* rated, and then allow all of them at the same time. And when the rating is closed,
* disable all of them at the same time. This is done in the settings-controller.
*
* TODO probably move to the project controller, allow changing this setting only
* if an admin
*
* TODO write test that the attribute cannot be changed by the project put endpoint
* by regular users
*/
@Post("/make-project-ratable")
@Authorized(UserRole.Root)
public async enableRatingForProject(): Promise<void> {
// TODO set allowRating of project
}

/**
* Rate a project
*
* TODO mvp: no update and delete, a created rating is a commitment to it.
* If there is time, add an update mechanism though.
*/
@Post("/rate")
@Authorized(UserRole.User)
public async createRating(
@Body() { data: RatingDTO }: { data: RatingDTO },
@CurrentUser() user: User,
): Promise<readonly RatingDTO[]> {
const rating = convertBetweenEntityAndDTO(RatingDTO, Rating);
const createdRating = await this._ratings.createRating(rating);
return convertBetweenEntityAndDTO(createdRating, RatingDTO);
}

/**
* Create criteria.
*/
@Post("/criteria")
@Authorized(UserRole.Root)
public async createCriteria(
@Body() { data: criteriaDTO }: { data: CriteriaDTO },
): Promise<readonly CriteriaDTO[]> {
const criteria = convertBetweenEntityAndDTO(criteriaDTO, Criteria);
const createdCriteria = await this._ratings.createCriteria();
return convertBetweenEntityAndDTO(createdCriteria, CriteriaDTO);
}

/**
* Update criteria.
*/
@Put("/criteria/:id")
@Authorized(UserRole.Root)
public async updateCriteria(
@Param("id") teamId: number,
@Body() { data: criteriaDTO }: { data: CriteriaDTO },
): Promise<TeamDTO> {
// TODO There is a TeamUpdateDTO. CriteriaUpdateDTO?
const team = convertBetweenEntityAndDTO(criteriaDTO, Criteria);
const updateTeam = await this._ratings.updateCriteria(team, user);
return convertBetweenEntityAndDTO(updateCriteria, CriteriaDTO);
}

/**
* Delete criteria.
*/
@Delete("/criteria/:id")
@Authorized(UserRole.Root)
public async deleteCriteria(
@Param("id") criteriaId: number,
@CurrentUser() user: User,
): Promise<SuccessResponseDTO> {
await this._ratings.deleteCriteriaByID(criteriaId, user);
const response = new SuccessResponseDTO();
response.success = true;
return response;
}

// TODO write test that all the root endpoints are not accessible by users
}
14 changes: 14 additions & 0 deletions backend/src/entities/criteria.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from "typeorm";
import { Longtext } from "./longtext";

@Entity()
export class Criteria {
@PrimaryGeneratedColumn()
public readonly id!: number;
@Column({ length: 1024 })
public title!: string;
@Longtext()
public description!: string;
}

// TODO Rating Project and Criteria DTO
11 changes: 11 additions & 0 deletions backend/src/entities/longtext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Column } from "typeorm";

export function Longtext() {
// Sqlite doesn't support longtext. I hate this, but it is what it is if we want
// to keep backend tests and prepare tilt for the upcoming Hackaburg as quickly
// as possible.
if (process.env.NODE_ENV === "test") {
return Column("text");
}
return Column("longtext");
}
17 changes: 17 additions & 0 deletions backend/src/entities/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from "typeorm";
import { Longtext } from "./longtext";
import { Team } from "./team";

@Entity()
export class Project {
@PrimaryGeneratedColumn()
public readonly id!: number;
@OneToOne(() => Team, { eager: true })
public team!: Team;
@Column({ length: 1024 })
public title!: string;
@Longtext()
public description!: string;
@Column()
public allowRating!: boolean;
}
21 changes: 21 additions & 0 deletions backend/src/entities/rating.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from "typeorm";
import { Criteria } from "./criteria";
import { Project } from "./project";
import { User } from "./user";

@Entity()
export class Rating {
@PrimaryGeneratedColumn()
public readonly id!: number;
@OneToOne(() => User, { eager: true })
public user!: User;
@OneToOne(() => Project, { eager: true })
public project!: Project;
@OneToOne(() => Criteria, { eager: true })
public critera!: Criteria;
@Column()
// 1 - 5
public rating!: number;
}

// TODO mvp: only create ratings, no update and delete
11 changes: 1 addition & 10 deletions backend/src/entities/team.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

function Longtext() {
// Sqlite doesn't support longtext. I hate this, but it is what it is if we want
// to keep backend tests and prepare tilt for the upcoming Hackaburg as quickly
// as possible.
if (process.env.NODE_ENV === "test") {
return Column("text");
}
return Column("longtext");
}
import { Longtext } from "./longtext";

@Entity()
export class Team {
Expand Down
12 changes: 12 additions & 0 deletions backend/src/middlewares/logging-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Middleware, ExpressMiddlewareInterface } from "routing-controllers";

@Middleware({ type: "before" })
export class LoggingMiddleware implements ExpressMiddlewareInterface {
use(request: any, response: any, next: (err: any) => any): void {
// TODO maybe remove this middleware, idk if I'll really need it.
// Or maybe keep it but only do this if debugging is on.
// But I'd want the proper logging object for this.
console.log("Incoming", request.method, request.url);
next(undefined);
}
}
122 changes: 122 additions & 0 deletions backend/src/services/project-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Inject, Service, Token } from "typedi";
import { Repository } from "typeorm";
import { IService } from ".";
import { DatabaseServiceToken, IDatabaseService } from "./database-service";
import { Project } from "../entities/project";
import {
ProjectResponseDTO,
convertBetweenEntityAndDTO,
} from "../controllers/dto";
import { User } from "../entities/user";

/**
* An interface describing user handling.
*/
export interface IProjectService extends IService {
/**
* Get all projects
*/
getAllProjects(): Promise<readonly Project[]>;
/**
* Create new project
*/
createProject(project: Project): Promise<Project>;
/**
* Update project
*/
updateProject(project: Project, user: User): Promise<Project>;
/**
* Get project by id
*/
getProjectByID(id: number): Promise<ProjectResponseDTO | undefined>;
/**
* Delete single project by id
*/
deleteProjectByID(id: number, currentUserId: User): Promise<void>;
}

/**
* A token used to inject a concrete user service.
*/
export const ProjectServiceToken = new Token<IProjectService>();

/**
* A service to handle users.
*/
@Service(ProjectServiceToken)
export class ProjectService implements IProjectService {
private _projects!: Repository<Project>;
private _users!: Repository<User>;

public constructor(
@Inject(DatabaseServiceToken) private readonly _database: IDatabaseService,
) {}

/**
* Sets up the user service.
*/
public async bootstrap(): Promise<void> {
this._projects = this._database.getRepository(Project);
this._users = this._database.getRepository(User);
}

/**
* Gets all projects.
*/
public async getAllProjects(): Promise<readonly Project[]> {
return this._database.getRepository(Project).find();
}

/**
* Updates a project.
* @param project The project to update
*/
public async updateProject(project: Project, user: User): Promise<Project> {
// TODO
await this.checkPermission(project, user);
// TODO allow changing allowRating only if admin
}

/**
* Creates a project.
* @param project The project to create
*/
public async createProject(project: Project): Promise<Project> {
// TODO
}

/**
* Gets a project by its id.
* @param id The id of the project
*/
public async getProjectByID(id: number): Promise<ProjectResponseDTO | undefined> {
const project = await this._projects.findOneBy({ id });
return project || undefined;
}

/**
* Deletes a project by its id.
* @param id The id of the project
*/
public async deleteProjectByID(id: number, currentUserId: User): Promise<void> {
const project = await this._projects.findOneBy({ id });

this.checkPermission(project, user);

await this._projects.delete(id);

return Promise.resolve();
}

/**
* Throw errors if the user is not allowed to modify/access the project.
*/
private async checkPermission(project: Project, user: User): Promise<void> {
const team = await this._teams.getTeamById(project.teamId)
if (!team.users.inclues(user.id)) {
// Tried to access a project belonging to a different team, forbidden
// TODO test
throw new NotFoundError()
}
}
}
Loading
Loading