Skip to content

Commit 653895e

Browse files
committed
Very unfinished wip for controllers and services for rating, criteria and project
1 parent 7b48e86 commit 653895e

4 files changed

Lines changed: 395 additions & 0 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Authorized, Delete, JsonController, NotFoundError } from "routing-controllers";
2+
import { Inject } from "typedi";
3+
import { UserRole } from "../entities/user-role";
4+
import { IProjectService, ProjectServiceToken } from "../services/project-service";
5+
6+
// TODO for every team, add a new project automatically with the correct teamId
7+
8+
@JsonController("/projects")
9+
export class ProjectController {
10+
public constructor(
11+
@Inject(ProjectServiceToken) private readonly _projects: IProjectService,
12+
) {}
13+
14+
/**
15+
* Update a project (mvp: create one project per team)
16+
*/
17+
@Put("/project/:id")
18+
@Authorized(UserRole.User)
19+
public async updateProject(
20+
@Param("id") projectId: number,
21+
@Body() { data: projectDTO }: { data: ProjectDTO },
22+
): Promise<TeamDTO> {
23+
// TODO ProjectUpdateDTO?
24+
const project = convertBetweenEntityAndDTO(projectDTO, Project);
25+
26+
// TODO how to make actual not found errors for incorrect ids?
27+
28+
const updateProject = await this._ratings.updateProject(project, user);
29+
return convertBetweenEntityAndDTO(updateProject, ProjectDTO);
30+
}
31+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Authorized, Delete, JsonController, ForbiddenError } from "routing-controllers";
2+
import { Inject } from "typedi";
3+
import { UserRole } from "../entities/user-role";
4+
import { SettingsServiceToken } from "../services/settings-service";
5+
import { SettingsServiceToken } from "../services/settings-service";
6+
7+
// The RatingController and RatingService group stuff concerning ratings and critiera
8+
// together. Feel free to separate, if you think that would be better.
9+
10+
@JsonController("/ratings")
11+
export class RatingController {
12+
public constructor(
13+
@Inject(SettingsServiceToken) private readonly _settings: ISettingsService,
14+
@Inject(RatingServiceToken) private readonly _ratings: IRatingService,
15+
) {}
16+
17+
/**
18+
* Allow users to rate a specific project (if ratings are enabled in the application
19+
* settings).
20+
*
21+
* By using the application setting, admins can prepare the projects that can be
22+
* rated, and then allow all of them at the same time. And when the rating is closed,
23+
* disable all of them at the same time. This is done in the settings-controller.
24+
*
25+
* TODO probably move to the project controller, allow changing this setting only
26+
* if an admin
27+
*
28+
* TODO write test that the attribute cannot be changed by the project put endpoint
29+
* by regular users
30+
*/
31+
@Post("/make-project-ratable")
32+
@Authorized(UserRole.Root)
33+
public async enableRatingForProject(): Promise<void> {
34+
// TODO set allowRating of project
35+
}
36+
37+
/**
38+
* Rate a project
39+
*
40+
* TODO mvp: no update and delete, a created rating is a commitment to it.
41+
* If there is time, add an update mechanism though.
42+
*/
43+
@Post("/rate")
44+
@Authorized(UserRole.User)
45+
public async createRating(
46+
@Body() { data: RatingDTO }: { data: RatingDTO },
47+
@CurrentUser() user: User,
48+
): Promise<readonly RatingDTO[]> {
49+
const rating = convertBetweenEntityAndDTO(RatingDTO, Rating);
50+
const createdRating = await this._ratings.createRating(rating);
51+
return convertBetweenEntityAndDTO(createdRating, RatingDTO);
52+
}
53+
54+
/**
55+
* Create criteria.
56+
*/
57+
@Post("/criteria")
58+
@Authorized(UserRole.Root)
59+
public async createCriteria(
60+
@Body() { data: criteriaDTO }: { data: CriteriaDTO },
61+
): Promise<readonly CriteriaDTO[]> {
62+
const criteria = convertBetweenEntityAndDTO(criteriaDTO, Criteria);
63+
const createdCriteria = await this._ratings.createCriteria();
64+
return convertBetweenEntityAndDTO(createdCriteria, CriteriaDTO);
65+
}
66+
67+
/**
68+
* Update criteria.
69+
*/
70+
@Put("/criteria/:id")
71+
@Authorized(UserRole.Root)
72+
public async updateCriteria(
73+
@Param("id") teamId: number,
74+
@Body() { data: criteriaDTO }: { data: CriteriaDTO },
75+
): Promise<TeamDTO> {
76+
// TODO There is a TeamUpdateDTO. CriteriaUpdateDTO?
77+
const team = convertBetweenEntityAndDTO(criteriaDTO, Criteria);
78+
const updateTeam = await this._ratings.updateCriteria(team, user);
79+
return convertBetweenEntityAndDTO(updateCriteria, CriteriaDTO);
80+
}
81+
82+
/**
83+
* Delete criteria.
84+
*/
85+
@Delete("/criteria/:id")
86+
@Authorized(UserRole.Root)
87+
public async deleteCriteria(
88+
@Param("id") criteriaId: number,
89+
@CurrentUser() user: User,
90+
): Promise<SuccessResponseDTO> {
91+
await this._ratings.deleteCriteriaByID(criteriaId, user);
92+
const response = new SuccessResponseDTO();
93+
response.success = true;
94+
return response;
95+
}
96+
97+
// TODO write test that all the root endpoints are not accessible by users
98+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { Inject, Service, Token } from "typedi";
2+
import { Repository } from "typeorm";
3+
import { IService } from ".";
4+
import { DatabaseServiceToken, IDatabaseService } from "./database-service";
5+
import { Project } from "../entities/project";
6+
import {
7+
ProjectResponseDTO,
8+
convertBetweenEntityAndDTO,
9+
} from "../controllers/dto";
10+
import { User } from "../entities/user";
11+
12+
/**
13+
* An interface describing user handling.
14+
*/
15+
export interface IProjectService extends IService {
16+
/**
17+
* Get all projects
18+
*/
19+
getAllProjects(): Promise<readonly Project[]>;
20+
/**
21+
* Create new project
22+
*/
23+
createProject(project: Project): Promise<Project>;
24+
/**
25+
* Update project
26+
*/
27+
updateProject(project: Project, user: User): Promise<Project>;
28+
/**
29+
* Get project by id
30+
*/
31+
getProjectByID(id: number): Promise<ProjectResponseDTO | undefined>;
32+
/**
33+
* Delete single project by id
34+
*/
35+
deleteProjectByID(id: number, currentUserId: User): Promise<void>;
36+
}
37+
38+
/**
39+
* A token used to inject a concrete user service.
40+
*/
41+
export const ProjectServiceToken = new Token<IProjectService>();
42+
43+
/**
44+
* A service to handle users.
45+
*/
46+
@Service(ProjectServiceToken)
47+
export class ProjectService implements IProjectService {
48+
private _projects!: Repository<Project>;
49+
private _users!: Repository<User>;
50+
51+
public constructor(
52+
@Inject(DatabaseServiceToken) private readonly _database: IDatabaseService,
53+
) {}
54+
55+
/**
56+
* Sets up the user service.
57+
*/
58+
public async bootstrap(): Promise<void> {
59+
this._projects = this._database.getRepository(Project);
60+
this._users = this._database.getRepository(User);
61+
}
62+
63+
/**
64+
* Gets all projects.
65+
*/
66+
public async getAllProjects(): Promise<readonly Project[]> {
67+
return this._database.getRepository(Project).find();
68+
}
69+
70+
/**
71+
* Updates a project.
72+
* @param project The project to update
73+
*/
74+
public async updateProject(project: Project, user: User): Promise<Project> {
75+
// TODO
76+
await this.checkPermission(project, user);
77+
// TODO allow changing allowRating only if admin
78+
}
79+
80+
/**
81+
* Creates a project.
82+
* @param project The project to create
83+
*/
84+
public async createProject(project: Project): Promise<Project> {
85+
// TODO
86+
}
87+
88+
/**
89+
* Gets a project by its id.
90+
* @param id The id of the project
91+
*/
92+
public async getProjectByID(id: number): Promise<ProjectResponseDTO | undefined> {
93+
const project = await this._projects.findOneBy({ id });
94+
return project || undefined;
95+
}
96+
97+
/**
98+
* Deletes a project by its id.
99+
* @param id The id of the project
100+
*/
101+
public async deleteProjectByID(id: number, currentUserId: User): Promise<void> {
102+
const project = await this._projects.findOneBy({ id });
103+
104+
this.checkPermission(project, user);
105+
106+
await this._projects.delete(id);
107+
108+
return Promise.resolve();
109+
}
110+
111+
/**
112+
* Throw errors if the user is not allowed to modify/access the project.
113+
*/
114+
private async checkPermission(project: Project, user: User): Promise<void> {
115+
const team = await this._teams.getTeamById(project.teamId)
116+
if (!team.users.inclues(user.id)) {
117+
// Tried to access a project belonging to a different team, forbidden
118+
// TODO test
119+
throw new NotFoundError()
120+
}
121+
}
122+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { Inject, Service, Token } from "typedi";
2+
import { Repository } from "typeorm";
3+
import { IService } from ".";
4+
import { DatabaseServiceToken, IDatabaseService } from "./database-service";
5+
import { Rating } from "../entities/rating";
6+
import {
7+
RatingResponseDTO,
8+
convertBetweenEntityAndDTO,
9+
} from "../controllers/dto";
10+
import { User } from "../entities/user";
11+
12+
export interface IRatingService extends IService {
13+
/**
14+
* Get all ratings
15+
*/
16+
getAllRatings(): Promise<readonly Rating[]>;
17+
/**
18+
* Create new rating
19+
*/
20+
createRating(rating: Rating): Promise<Rating>;
21+
/**
22+
* Update rating
23+
*/
24+
updateRating(rating: Rating): Promise<Rating>;
25+
/**
26+
* Get rating by id
27+
*/
28+
getRatingByID(id: number): Promise<RatingResponseDTO | undefined>;
29+
/**
30+
* Delete single rating by id
31+
*/
32+
deleteRatingByID(id: number, currentUserId: User): Promise<void>;
33+
/**
34+
* Request to join a rating
35+
*/
36+
requestToJoinRating(ratingId: number, user: User): Promise<void>;
37+
}
38+
39+
/**
40+
* A token used to inject a concrete user service.
41+
*/
42+
export const RatingServiceToken = new Token<IRatingService>();
43+
44+
/**
45+
* A service to handle users.
46+
*/
47+
@Service(RatingServiceToken)
48+
export class RatingService implements IRatingService {
49+
private _ratings!: Repository<Rating>;
50+
private _users!: Repository<User>;
51+
52+
public constructor(
53+
@Inject(DatabaseServiceToken) private readonly _database: IDatabaseService,
54+
) {}
55+
56+
/**
57+
* Sets up the user service.
58+
*/
59+
public async bootstrap(): Promise<void> {
60+
this._ratings = this._database.getRepository(Rating);
61+
this._users = this._database.getRepository(User);
62+
}
63+
64+
/**
65+
* Gets all ratings.
66+
*/
67+
public async getAllRatings(): Promise<readonly Rating[]> {
68+
return this._database.getRepository(Rating).find();
69+
}
70+
71+
/**
72+
* Updates a rating.
73+
* @param rating The rating to update
74+
*/
75+
public async updateRating(rating: Rating, user: User): Promise<Rating> {
76+
// TODO validate, throw Errors
77+
78+
const originRating = await this._ratings.findOneBy({ id: rating.id });
79+
80+
// TODO only if user matches
81+
await this.checkPermission(rating, user);
82+
const originRatingUser = originRating.users;
83+
if (user.id != originRatingUser.id) {
84+
throw new Error("")
85+
}
86+
87+
return this._ratings.save(rating);
88+
}
89+
90+
/**
91+
* Creates a rating.
92+
* @param rating The rating to create
93+
*/
94+
public async createRating(rating: Rating): Promise<Rating> {
95+
// TODO validate
96+
this.checkPermission(rating, user);
97+
return this._ratings.save(rating);
98+
}
99+
100+
/**
101+
* Gets a rating by its id.
102+
* @param id The id of the rating
103+
*/
104+
public async getRatingByID(id: number): Promise<RatingResponseDTO | undefined> {
105+
const rating = await this._ratings.findOneBy({ id });
106+
return rating || undefined;
107+
}
108+
109+
/**
110+
* Deletes a rating by its id.
111+
* @param id The id of the rating
112+
*/
113+
public async deleteRatingByID(id: number, currentUserId: User): Promise<void> {
114+
const rating = await this._ratings.findOneBy({ id });
115+
116+
await this.checkPermission(rating, user);
117+
118+
await this._ratings.delete(id);
119+
120+
return Promise.resolve();
121+
}
122+
123+
private async checkPermission(rating: Rating, user: User): Promise<void> {
124+
// TODO use this._database.blabla instead of _settings and such
125+
126+
const settings = await this._settings.getSettings();
127+
if (!settings.allowRating) {
128+
// TODO test
129+
throw new ForbiddenError("Cannot create rating due to application settings")
130+
}
131+
132+
const project = await this._projects.getProject(data.id);
133+
if (!project.allowRating) {
134+
// TODO test
135+
throw new ForbiddenError("Creating a rating for this project is not allowed")
136+
}
137+
138+
const team = await this._teams.getTeamById(project.teamId)
139+
if (team.users.inclues(user.id)) {
140+
// TODO test
141+
throw new ForbiddenError("You can't rate your own project")
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)