Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
5b7eb46
Upgrade dependencies, quickstart
sezanzeb Feb 21, 2026
1f118e6
Merge branch 'main' into voting-feature
sezanzeb Mar 1, 2026
b447fde
some small unix signal handler clarification
sezanzeb Mar 1, 2026
f5592ca
database entities
sezanzeb Mar 4, 2026
faf8a32
prettier
sezanzeb Mar 4, 2026
5f9e739
Add DTOs
sezanzeb Mar 4, 2026
7b48e86
security upgrades
sezanzeb Mar 4, 2026
653895e
Very unfinished wip for controllers and services for rating, criteria…
sezanzeb Mar 15, 2026
bf43af6
Added a few missing imports and fixed some types
sezanzeb Mar 18, 2026
e194e23
wip
Mar 29, 2026
df16c8c
ignor epackag-elock
Mar 29, 2026
23669a8
wip
Mar 29, 2026
86b5079
fix: use NotFoundError instead of ForbiddenError for not-found cases …
Copilot Mar 29, 2026
0c12819
refactor: split rating-controller into rating-controller and criteria…
Copilot Mar 29, 2026
0bd1f79
fix: correct _ratings→_projects, add NotFoundError, fix imports/types…
Copilot Mar 29, 2026
81ac6a0
Merge pull request #113 from hackaburg/copilot/fix-notfound-error-rep…
sezanzeb Mar 29, 2026
c4c38a7
feat: merge allowRating into PUT /projects/project/:id, restrict to a…
Copilot Mar 29, 2026
92562a9
Merge pull request #114 from hackaburg/copilot/update-make-project-ra…
sezanzeb Mar 29, 2026
d3aaa4b
fix: rating service permission checks, ownership enforcement, and val…
Copilot Mar 29, 2026
821b656
remoe todos
Mar 29, 2026
48ed2e6
restore yarn.lock
Mar 29, 2026
1513d6e
Merge pull request #115 from hackaburg/copilot/fix-rating-permission-…
sezanzeb Mar 29, 2026
b337117
Add ICriteriaService and use it in CriteriaController
Copilot Mar 29, 2026
62620c2
Merge pull request #116 from hackaburg/copilot/add-icriteria-service
sezanzeb Mar 29, 2026
4de21d6
createProject
Mar 29, 2026
8b39ecb
test: add tests for checkPermission errors, admin-only createRating, …
Copilot Mar 29, 2026
3282920
test: replace metadata-based auth tests with real HTTP request tests
Copilot Mar 29, 2026
0660f67
refactor: use HttpService.isActionAuthorized in authorization tests, …
Copilot Mar 29, 2026
90b7d45
Merge pull request #117 from hackaburg/copilot/write-tests-for-error-…
sezanzeb Mar 29, 2026
fab07b0
chore: plan rating feature fixes
Copilot Mar 29, 2026
7c0ed54
fix: rating authorization, duplicates, GET endpoints, ManyToOne relat…
Copilot Mar 29, 2026
c5c6361
Changes before error encountered
Copilot Mar 29, 2026
5f9f2c0
reset yarn.lock, voteCount -> ratingCount
Mar 29, 2026
39ee2de
extend quickstart info
Mar 31, 2026
d6a45eb
Merge pull request #118 from hackaburg/copilot/analyze-hackathon-regi…
sezanzeb Mar 31, 2026
e3e9ec4
improve quickstart, add usermod to package.json scripts
Mar 31, 2026
e5fd2ea
Improved directory structure and naming for questions and settings
sezanzeb Apr 1, 2026
078de30
Fix tests
sezanzeb Apr 3, 2026
23af330
pipeline minimatch fix. starting work on project-rating settings
sezanzeb Apr 3, 2026
f787a35
feat: extend ApiClient with criteria, projects, and ratings endpoints
Copilot Apr 3, 2026
8751e7e
Merge pull request #120 from hackaburg/copilot/check-settings-archite…
sezanzeb Apr 3, 2026
34293b3
Criteria can be added and edited
sezanzeb Apr 3, 2026
a82f2cf
Hopefully correct singular and plural usage, rewrote getRatingResults
sezanzeb Apr 4, 2026
2bdca9a
Fix test
sezanzeb Apr 4, 2026
c887540
Complicated project-rating-settings
sezanzeb Apr 4, 2026
7298a1a
title and desc edits require save, add and delete not, dumb
sezanzeb Apr 4, 2026
718a5c2
Allow rating projects switch functional
sezanzeb Apr 4, 2026
8c50be3
Response. Teams is not beta anymore
sezanzeb Apr 4, 2026
0f24730
Start work on projects page
sezanzeb Apr 5, 2026
7f903b2
Fix project entity JoinColumn
sezanzeb Apr 5, 2026
f4504ff
getAllProjects only sends projects that are owned or can be rated
sezanzeb Apr 5, 2026
e9ce1a4
Add more assertions
sezanzeb Apr 5, 2026
23e54be
Every team gets a default project
sezanzeb Apr 5, 2026
7b3b2d7
Improve project-service.spec
sezanzeb Apr 5, 2026
a5361f4
Use team image as fallback in ui
sezanzeb Apr 5, 2026
d0381d7
Project editable
sezanzeb Apr 6, 2026
fa569e2
Add divider below headline of teams.tsx and projects.tsx
sezanzeb Apr 6, 2026
9116812
Some styling stuff
sezanzeb Apr 6, 2026
c4ae7a4
Removed redundant flex container wrappers
sezanzeb Apr 6, 2026
c90ae4d
Admins can enable rating for individual projects
sezanzeb Apr 6, 2026
850d7ce
View team wip
sezanzeb Apr 6, 2026
28d517d
View team wip
sezanzeb Apr 6, 2026
d8d2834
View team wip
sezanzeb Apr 6, 2026
352bc09
View team
sezanzeb Apr 6, 2026
6caa370
file renaming
sezanzeb Apr 6, 2026
2f299b5
wip, pageheader component
sezanzeb Apr 6, 2026
95d1f4f
Use PageHeader almost everywhere, fix updateProject id
sezanzeb Apr 6, 2026
9cf1639
Margins, ViewProject project null fix
sezanzeb Apr 6, 2026
8c7adab
Prevent impersonation of other users when casting votes. 2025 -> 2026
sezanzeb Apr 6, 2026
4f58040
Ratings can be cast by users in the frontend
sezanzeb Apr 6, 2026
e00f3ef
Fix backend tests
sezanzeb Apr 6, 2026
33eb7e7
rewrite rating-service.spec.ts to use TestDatabaseService
Copilot Apr 6, 2026
656bb32
Merge pull request #121 from hackaburg/copilot/update-rating-service-…
sezanzeb Apr 7, 2026
06920c0
Show rating results on project list for admins
sezanzeb Apr 7, 2026
64743f9
Fix test
sezanzeb Apr 7, 2026
4bee27a
Prettier
sezanzeb Apr 7, 2026
b106631
Only admitted users may rate projects
sezanzeb Apr 7, 2026
969d0c1
Dont tell admins they are part of every project
sezanzeb Apr 7, 2026
7942a56
Fix all TypeScript errors from yarn run frontend::typecheck
Copilot Apr 7, 2026
24326ff
Merge branch 'voting-feature' into copilot/fix-typecheck-issues
sezanzeb Apr 7, 2026
7926786
Merge pull request #122 from hackaburg/copilot/fix-typecheck-issues
sezanzeb Apr 7, 2026
4922eb4
Even prettier
sezanzeb Apr 7, 2026
9e117eb
typefix
sezanzeb Apr 7, 2026
2fdc387
prettier
sezanzeb Apr 7, 2026
7d5554e
yarn upgrade
sezanzeb Apr 7, 2026
e86031c
lint
sezanzeb Apr 8, 2026
d6c2c85
prettier
sezanzeb Apr 8, 2026
6af3ac0
Adding types: jest fixes the minimatch error, I dont know, I hate tsc…
sezanzeb Apr 8, 2026
e0740f6
Fix minimatch type error properly, upgrade webpack-dev stuff
sezanzeb Apr 8, 2026
1ce8610
Fix rating selection and alignment
sezanzeb Apr 8, 2026
642d7e9
Fix tooltip location
sezanzeb Apr 8, 2026
1bd190a
prettier
sezanzeb Apr 8, 2026
a20a551
lint stuff
sezanzeb Apr 8, 2026
bc0ceb3
rating help text
sezanzeb Apr 8, 2026
b083753
PRETTIER!
sezanzeb Apr 8, 2026
24f9ff8
copilot review stuff
sezanzeb Apr 8, 2026
78794e0
removed unused stuff
sezanzeb Apr 8, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
package-lock.json
node_modules/
dist/
coverage/
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

Yet another hackathon registration system.

[Docker Development quickstart.md](quickstart.md)

## Motivation

Like many other hackathons, we previously used [Quill](https://github.com/techx/quill) for our application process, which worked really well for us in the past. Especially Quill's process was a blessing: an application consists of two steps, the profile creation and, once an attendee was admitted to the event, the spot confirmation. We attended different events that used different processes and found this to be easy for both the attendees and organizers.
Expand Down
84 changes: 84 additions & 0 deletions backend/src/controllers/criterion-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
Authorized,
Delete,
Get,
JsonController,
Param,
Put,
Post,
Body,
} from "routing-controllers";
import { Inject } from "typedi";
import { UserRole } from "../entities/user-role";
import {
CriterionServiceToken,
ICriterionService,
} from "../services/criterion-service";
import {
CriterionDTO,
SuccessResponseDTO,
convertBetweenEntityAndDTO,
} from "./dto";
import { Criterion } from "../entities/criterion";

@JsonController("/criteria")
export class CriterionController {
public constructor(
@Inject(CriterionServiceToken)
private readonly _criterion: ICriterionService,
) {}

/**
* Get all criteria.
*/
@Get("/")
@Authorized(UserRole.User)
public async getAllCriteria(): Promise<CriterionDTO[]> {
const criteria = await this._criterion.getAllCriteria();
return criteria.map((c) => convertBetweenEntityAndDTO(c, CriterionDTO));
}

/**
* Create a criterion.
*/
@Post("/")
@Authorized(UserRole.Root)
public async createCriterion(
@Body() { data: criterionDTO }: { data: CriterionDTO },
): Promise<CriterionDTO> {
const criterion = convertBetweenEntityAndDTO(criterionDTO, Criterion);
const createdCriterion = await this._criterion.createCriterion(criterion);
return convertBetweenEntityAndDTO(createdCriterion, CriterionDTO);
}

/**
* Update criteria.
*/
@Put("/:id")
@Authorized(UserRole.Root)
public async updateCriterion(
@Param("id") criterionId: number,
@Body() { data: criterionDTO }: { data: CriterionDTO },
): Promise<CriterionDTO> {
const criterion = convertBetweenEntityAndDTO(
{ ...criterionDTO, id: criterionId },
Criterion,
);
const updateCriterion = await this._criterion.updateCriterion(criterion);
return convertBetweenEntityAndDTO(updateCriterion, CriterionDTO);
}

/**
* Delete criteria.
*/
@Delete("/:id")
@Authorized(UserRole.Root)
public async deleteCriterion(
@Param("id") criterionId: number,
): Promise<SuccessResponseDTO> {
await this._criterion.deleteCriterionByID(criterionId);
const response = new SuccessResponseDTO();
response.success = true;
return response;
}
}
86 changes: 86 additions & 0 deletions backend/src/controllers/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
IsOptional,
IsString,
IsUrl,
Max,
MaxLength,
Min,
MinLength,
ValidateNested,
} from "class-validator";
Expand Down Expand Up @@ -89,6 +91,9 @@ export class ApplicationSettingsDTO implements DTO<ApplicationSettings> {
@IsNumber()
@Expose()
public hoursToConfirm!: number;
@IsBoolean()
@Expose()
public allowRatingProjects!: boolean;
}

export class FrontendSettingsDTO implements DTO<FrontendSettings> {
Expand Down Expand Up @@ -561,3 +566,84 @@ export class TeamUpdateDTO {
@Expose()
public description!: string;
}

export class CriterionDTO {
@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;
@Expose()
public image!: string;
}

export class ProjectUpdateDTO {
@Expose()
public readonly id!: number;
@Expose()
public title!: string;
@Expose()
public description!: string;
@Expose()
public allowRating!: boolean;
@Expose()
public image!: string;
}

export class RatingDTO {
@Expose()
public readonly id!: number;
@Expose()
@Type(() => UserDTO)
@ValidateNested()
public user!: UserDTO;
@Expose()
@Type(() => ProjectDTO)
@ValidateNested()
public project!: ProjectDTO;
@Expose()
@Type(() => CriterionDTO)
@ValidateNested()
public criterion!: CriterionDTO;
@Expose()
@IsInt()
@Min(1)
@Max(5)
public rating!: number;
}

class CriterionAvgDTO {
@Expose()
@Type(() => CriterionDTO)
public criterion!: CriterionDTO;
@Expose()
public average!: number;
}

// Do not send all ratings to the client,
// because peoples opinion on the projects should be anonymous
export class ProjectRatingResultDTO {
@Expose()
@Type(() => ProjectDTO)
public project!: ProjectDTO;
@IsArray()
@Type(() => CriterionAvgDTO)
@Expose()
public averagesPerCriterion!: CriterionAvgDTO[];
}
86 changes: 86 additions & 0 deletions backend/src/controllers/projects-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
Authorized,
Get,
JsonController,
NotFoundError,
Put,
Param,
Body,
CurrentUser,
} from "routing-controllers";
import { Inject } from "typedi";
import { UserRole } from "../entities/user-role";
import {
IProjectService,
ProjectServiceToken,
} from "../services/project-service";
import {
ProjectDTO,
ProjectUpdateDTO,
convertBetweenEntityAndDTO,
} from "./dto";
import { Project } from "../entities/project";
import { User } from "../entities/user";

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

/**
* Get all projects.
*/
@Get("/")
@Authorized(UserRole.User)
public async getAllProjects(
@CurrentUser() user: User,
): Promise<ProjectDTO[]> {
const projects = await this._projects.getAllProjects(user);
return projects.map((p) => convertBetweenEntityAndDTO(p, ProjectDTO));
}

/**
* Get project by id.
* @param id The id of the project
*/
@Get("/:id")
@Authorized(UserRole.User)
public async getProjectByID(@Param("id") id: number): Promise<ProjectDTO> {
const project = await this._projects.getProjectByID(id);

if (project == null) {
throw new NotFoundError(`no project with id ${id}`);
}

return convertBetweenEntityAndDTO(project, ProjectDTO);
}

/**
* Update a project (mvp: create one project per team)
*/
@Put("/:id")
@Authorized(UserRole.User)
public async updateProject(
@Param("id") projectId: number,
@Body() { data: projectDTO }: { data: ProjectUpdateDTO },
@CurrentUser() user: User,
): Promise<ProjectDTO> {
const existing = await this._projects.getProjectByID(projectId);

if (existing == null) {
throw new NotFoundError();
}

const project = convertBetweenEntityAndDTO(
{
...projectDTO,
id: projectId,
},
Project,
);

const updatedProject = await this._projects.updateProject(project, user);
return convertBetweenEntityAndDTO(updatedProject, ProjectDTO);
}
}
73 changes: 73 additions & 0 deletions backend/src/controllers/rating-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
Authorized,
JsonController,
CurrentUser,
Get,
Post,
Body,
Param,
} from "routing-controllers";
import { Inject } from "typedi";
import { UserRole } from "../entities/user-role";
import { RatingServiceToken, IRatingService } from "../services/rating-service";
import {
RatingDTO,
ProjectRatingResultDTO,
convertBetweenEntityAndDTO,
} from "./dto";
import { User } from "../entities/user";
import { Rating } from "../entities/rating";

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

/**
* Get aggregated rating results grouped by project and criteria.
*/
@Get("/by-project/:id")
@Authorized(UserRole.User)
public async getUsersRatingsForProject(
@Param("id") projectId: number,
@CurrentUser() user: User,
): Promise<RatingDTO[]> {
const results = await this._ratings.getUsersRatingsForProject(
projectId,
user,
);
return results.map((r) => convertBetweenEntityAndDTO(r, RatingDTO));
}

/**
* Get aggregated rating results grouped by project and criteria.
*/
@Get("/results")
@Authorized(UserRole.Root)
public async getRatingResults(): Promise<ProjectRatingResultDTO[]> {
const results = await this._ratings.getRatingResults();
return results.map((r) =>
convertBetweenEntityAndDTO(r, ProjectRatingResultDTO),
);
}

/**
* Rate a project
*/
@Post("/rate")
@Authorized(UserRole.User)
public async rate(
@Body() { data: ratingDTO }: { data: RatingDTO },
@CurrentUser() user: User,
): Promise<RatingDTO> {
const rating = convertBetweenEntityAndDTO(ratingDTO, Rating);

// Ensure ratings cannot be cast for other users,
// write the requesting user into it.
rating.user = user;

const createdRating = await this._ratings.upsertRating(rating, user);
return convertBetweenEntityAndDTO(createdRating, RatingDTO);
}
}
5 changes: 5 additions & 0 deletions backend/src/entities/application-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
} from "typeorm";
import { FormSettings } from "./form-settings";

// TODO all other settings are part of the settings table, whereas ApplicationSettings
// is a separate table. Move into settings table just like EmailSettings.

@Entity()
export class ApplicationSettings {
@PrimaryGeneratedColumn()
Expand All @@ -26,4 +29,6 @@ export class ApplicationSettings {
public allowProfileFormUntil!: Date;
@Column()
public hoursToConfirm!: number;
@Column({ default: false })
public allowRatingProjects!: boolean;
}
12 changes: 12 additions & 0 deletions backend/src/entities/criterion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { Longtext } from "./longtext";

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