Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f7fd2a7
wip, one request, one team, via foreign keys
sezanzeb Apr 8, 2026
537c065
project image fix, notification when saving team or project, readme s…
sezanzeb Apr 8, 2026
3a12ffd
readme
sezanzeb Apr 8, 2026
a62f678
readme
sezanzeb Apr 8, 2026
abd1fd5
change screenshot-1.jpg
sezanzeb Apr 8, 2026
b1b4379
fix docs link
sezanzeb Apr 8, 2026
f2bd846
Merge branch 'main' into teams2
sezanzeb Apr 11, 2026
299124c
backend tests passing
sezanzeb Apr 11, 2026
babc71a
lint and typecheck
sezanzeb Apr 11, 2026
e8bd20c
Some work on the frontend
sezanzeb Apr 11, 2026
9cd0b92
typecheck
sezanzeb Apr 11, 2026
ac55538
eager load user.team
sezanzeb Apr 11, 2026
b973f6c
lasjdflksajd
sezanzeb Apr 11, 2026
76f75df
fix user.team missing in token response
sezanzeb Apr 11, 2026
5e88293
wip
sezanzeb Apr 12, 2026
71e71eb
potentially working
sezanzeb Apr 12, 2026
12b5949
wip team.owner
sezanzeb Apr 12, 2026
740e5ce
various
sezanzeb Apr 12, 2026
e0cd89f
various
sezanzeb Apr 12, 2026
c129d71
Add types to StackWithBorder and TeamMember, fix all typecheck errors…
Copilot Apr 12, 2026
688e098
add setOwner to mock-teams-service
sezanzeb Apr 12, 2026
0259f85
wip auth.spec.ts
sezanzeb Apr 12, 2026
ed21c5f
wip auth.spec.ts
sezanzeb Apr 12, 2026
4d8b226
wip auth.spec.ts
sezanzeb Apr 12, 2026
98bb0fc
wip auth.spec.ts
sezanzeb Apr 12, 2026
b79f486
auth.spec.ts done
sezanzeb Apr 12, 2026
8b56b4b
lint, prettier
sezanzeb Apr 12, 2026
a4c983f
reload parent component instead of history go
sezanzeb Apr 12, 2026
7fdc4a8
copilot review
sezanzeb Apr 12, 2026
2584796
copilot review
sezanzeb Apr 12, 2026
7d276da
Add tests for `// TODO test` comments; remove resolved TODOs (#126)
Copilot Apr 12, 2026
cbf1ba7
add test
sezanzeb Apr 12, 2026
52e1e72
fix recursion depth error when creating team. Some css fixes
sezanzeb Apr 13, 2026
80ca3bd
small stuff
sezanzeb Apr 13, 2026
95b835f
unused import
sezanzeb Apr 13, 2026
3aeef99
user.team set after team created, warning if not allowed to rate
sezanzeb Apr 13, 2026
844ce4f
prettier
sezanzeb Apr 13, 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
324 changes: 29 additions & 295 deletions README.md

Large diffs are not rendered by default.

39 changes: 38 additions & 1 deletion backend/src/controllers/application-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,10 @@ export class ApplicationController {
@Authorized(UserRole.User)
public async createTeam(
@Body() { data: teamDTO }: { data: TeamRequestDTO },
@CurrentUser() user: User,
): Promise<TeamDTO> {
const team = convertBetweenEntityAndDTO(teamDTO, Team);
const createdTeam = await this._teams.createTeam(team);
const createdTeam = await this._teams.createTeam(team, user);
return convertBetweenEntityAndDTO(createdTeam, TeamDTO);
}

Expand Down Expand Up @@ -314,6 +315,42 @@ export class ApplicationController {
return response;
}

/**
* Remove a user from a team.
* @param teamId The id of the team
* @param userId The id of the user
*/
@Delete("/team/:teamId/members/:userId")
@Authorized(UserRole.User)
public async removeUserFromTeam(
@Param("teamId") teamId: number,
@Param("userId") userId: number,
@CurrentUser() user: User,
): Promise<SuccessResponseDTO> {
await this._teams.removeUserFromTeam(teamId, userId, user);
const response = new SuccessResponseDTO();
response.success = true;
return response;
}

/**
* Set the owner of a team
* @param teamId The id of the team
* @param userId The id of the new owner
*/
@Put("/team/:teamId/owner/:userId")
@Authorized(UserRole.User)
public async setOwner(
@Param("teamId") teamId: number,
@Param("userId") userId: number,
@CurrentUser() user: User,
): Promise<SuccessResponseDTO> {
await this._teams.setOwner(teamId, userId, user);
const response = new SuccessResponseDTO();
response.success = true;
return response;
}

/**
* Get team by id.
* @param id The id of the team
Expand Down
66 changes: 45 additions & 21 deletions backend/src/controllers/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,10 @@ export class UserDetailsRepsonseDTO {
public email!: string;
@Expose()
public role!: UserRole;
@Expose()
public teamRequest!: TeamDTO | null;
@Expose()
public team!: TeamDTO | null;
}

export class UserDTO {
Expand Down Expand Up @@ -445,12 +449,22 @@ export class UserDTO {
public checkedIn!: boolean;
@Expose()
public profileSubmitted!: boolean;
@Expose()
@Type(() => TeamDTO)
@ValidateNested()
public teamRequest!: TeamDTO | null;
@Expose()
@Type(() => TeamDTO)
@ValidateNested()
public team!: TeamDTO | null;
Comment thread
sezanzeb marked this conversation as resolved.
}

export class UserTokenResponseDTO {
@Expose()
public token!: string;
@Expose()
@Type(() => UserDTO)
@ValidateNested()
public user!: UserDTO;
}

Expand Down Expand Up @@ -498,16 +512,25 @@ export class UserListDto {
@Expose()
public id!: number;
@Expose()
public name!: string;
public firstName!: string;
@Expose()
public lastName!: string;
}

export class UserResponseDto {
@Expose()
public id!: number;
@Expose()
public firstName!: string;
@Expose()
public lastName!: string;
}

export class ApplicationDTO {
@Expose()
@Type(() => UserDTO)
public user!: UserDTO;
@Expose()
public teams!: string[];
@Expose()
@Type(() => AnswerDTO)
public answers!: AnswerDTO[];
}
Expand All @@ -522,20 +545,22 @@ export class IDRequestDTO implements IApiRequest<number> {
public data!: number;
}

export class UserResponseDto {
@Expose()
public id!: number;
@Expose()
public name!: string;
}

export class TeamDTO {
@Expose()
public id!: number;
@Expose()
public title!: string;
@Expose()
public users?: string[];
@Type(() => UserResponseDto)
public owner!: UserResponseDto;
@Expose()
@Type(() => UserResponseDto)
@ValidateNested()
public users!: UserResponseDto[];
@Expose()
@Type(() => UserResponseDto)
@ValidateNested()
public requests!: UserResponseDto[];
@Expose()
public teamImg!: string;
@Expose()
Expand All @@ -548,23 +573,24 @@ export class TeamResponseDTO {
@Expose()
public title!: string;
@Expose()
@Type(() => UserResponseDto)
public users?: UserResponseDto[];
@Expose()
public teamImg!: string;
@Expose()
public description!: string;
@Expose()
@Type(() => UserResponseDto)
public requests?: UserResponseDto[];
public owner!: UserResponseDto;
@Expose()
@Type(() => UserResponseDto)
public users!: UserResponseDto[];
@Expose()
@Type(() => UserResponseDto)
public requests!: UserResponseDto[];
}

export class TeamRequestDTO {
@Expose()
public title!: string;
@Expose()
public users?: number[];
@Expose()
public teamImg!: string;
@Expose()
public description!: string;
Expand All @@ -576,8 +602,6 @@ export class TeamUpdateDTO {
@Expose()
public title!: string;
@Expose()
public users?: number[];
@Expose()
public teamImg!: string;
@Expose()
public description!: string;
Expand Down Expand Up @@ -626,9 +650,9 @@ export class RatingDTO {
@Expose()
public readonly id!: number;
@Expose()
@Type(() => UserDTO)
@Type(() => UserResponseDto)
@ValidateNested()
public user!: UserDTO;
public user!: UserResponseDto;
@Expose()
@Type(() => ProjectDTO)
@ValidateNested()
Expand Down
6 changes: 0 additions & 6 deletions backend/src/controllers/users-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,6 @@ export class UsersController {
const response = new UserTokenResponseDTO();
response.token = this._users.generateLoginToken(user);
response.user = convertBetweenEntityAndDTO(user, UserDTO);
const userDetails = await this._users.getUser(user.email);
response.user.firstName = userDetails.firstName;
response.user.lastName = userDetails.lastName;
return response;
}

Expand All @@ -166,9 +163,6 @@ export class UsersController {
const response = new UserTokenResponseDTO();
response.token = this._users.generateLoginToken(user);
response.user = convertBetweenEntityAndDTO(user, UserDTO);
const userDetails = await this._users.getUser(user.email);
response.user.firstName = userDetails.firstName;
response.user.lastName = userDetails.lastName;
return response;
}

Expand Down
2 changes: 1 addition & 1 deletion backend/src/entities/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Team } from "./team";
export class Project {
@PrimaryGeneratedColumn()
public readonly id!: number;
@ManyToOne(() => Team, { eager: true })
@ManyToOne(() => Team, { eager: true, onDelete: "CASCADE" })
@JoinColumn()
public team!: Team;
@Column({ length: 1024 })
Expand Down
49 changes: 42 additions & 7 deletions backend/src/entities/team.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,56 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import {
Column,
Entity,
PrimaryGeneratedColumn,
OneToMany,
OneToOne,
JoinColumn,
} from "typeorm";
import { Longtext } from "./longtext";
import { User } from "./user";

@Entity()
export class Team {
@PrimaryGeneratedColumn()
public readonly id!: number;
@Column({ length: 1024 })
public title!: string;
// TODO many-to-many instead of simple-array (array of userId strings)
@Column("simple-array")
public users!: string[];
// TODO rename teamImg to image
@Column()
public teamImg!: string;
@Longtext()
public description!: string;
// TODO many-to-many instead of simple-array (array of userId strings)
@Column("simple-array")
public requests!: string[];
// The owner also has to have their user.team property set to this team.
// Beware that this is not eagerly loaded, because it will throw recursion depth
// errors due to user.team being eagerly loaded already. Add it to "relations"
// when doing database queries instead.
@OneToOne(() => User)
@JoinColumn()
public owner!: User;
@OneToMany(() => User, (user) => user.teamRequest)
public requests!: User[];
@OneToMany(() => User, (user) => user.team)
public users!: User[];

Comment thread
sezanzeb marked this conversation as resolved.
/**
* List of user ids that are part of the team.
*/
public userIds(): number[] {
if (!this.users) {
return [];
}

return this.users.map(({ id }) => id);
}

/**
* List of user ids that requested to join the team.
*/
public requestUserIds(): number[] {
if (!this.requests) {
return [];
}

return this.requests.map(({ id }) => id);
}
}
14 changes: 14 additions & 0 deletions backend/src/entities/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import {
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
ManyToOne,
} from "typeorm";
import { UserRole } from "./user-role";
import { Team } from "./team";

@Entity()
export class User {
Expand Down Expand Up @@ -45,4 +47,16 @@ export class User {
public declined!: boolean;
@Column({ default: false })
public checkedIn!: boolean;
@ManyToOne(() => Team, (team) => team.requests, {
nullable: true,
eager: true,
onDelete: "SET NULL",
})
public teamRequest: Team | null = null;
@ManyToOne(() => Team, (team) => team.users, {
nullable: true,
eager: true,
onDelete: "SET NULL",
})
public team: Team | null = null;
}
9 changes: 0 additions & 9 deletions backend/src/services/application-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
} from "./question-service";
import { ISettingsService, SettingsServiceToken } from "./settings-service";
import { IUserService, UserServiceToken } from "./user-service";
import { Team } from "../entities/team";

/**
* A form containing questions and given answers.
Expand All @@ -41,7 +40,6 @@ export interface IRawAnswer {
*/
export interface IApplication {
user: User;
teams: string[];
answers: readonly Answer[];
}

Expand Down Expand Up @@ -121,7 +119,6 @@ export const ApplicationServiceToken = new Token<IApplicationService>();
@Service(ApplicationServiceToken)
export class ApplicationService implements IApplicationService {
private _answers!: Repository<Answer>;
private _teams!: Repository<Team>;

constructor(
@Inject(QuestionGraphServiceToken)
Expand All @@ -138,7 +135,6 @@ export class ApplicationService implements IApplicationService {
*/
public async bootstrap(): Promise<void> {
this._answers = this._database.getRepository(Answer);
this._teams = this._database.getRepository(Team);
}

/**
Expand Down Expand Up @@ -545,13 +541,8 @@ export class ApplicationService implements IApplicationService {
}
}

const allTeams = await this._teams.find();

const applications = allUsers.map<IApplication>((user) => ({
answers: answersByUserID.get(user.id) ?? [],
teams: allTeams
.filter((team) => team.users.toString().includes(user.id.toString()))
.map((team) => team.title),
user,
}));

Expand Down
Loading
Loading