Skip to content
Merged
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
650775a
style: change font and polish UI
markstockhausen Jan 27, 2026
79e45a2
style: update font weight
markstockhausen Jan 27, 2026
430e1c2
style: conform to Prettier formatting
markstockhausen Feb 3, 2026
73a402b
Implement optimized Scheduling Service and Pipeline Logic
markstockhausen Feb 3, 2026
8850cf6
style: update comments
markstockhausen Feb 3, 2026
fe676c3
Implement AI suggestions
markstockhausen Feb 9, 2026
b199432
Update Axios, Optimise scheduling pipeline
markstockhausen Feb 9, 2026
67d6e58
Optimise pipeline logic and clean up code
markstockhausen Feb 10, 2026
60d7f66
Optimise pipeline
markstockhausen Feb 10, 2026
6251a3a
Update logic for bidirectional relationship types
markstockhausen Feb 10, 2026
e683410
Streamline implementation
markstockhausen Feb 10, 2026
141bebd
Conform to Prettier formatting
markstockhausen Feb 10, 2026
64c4f20
Implement AI suggestions
markstockhausen Feb 17, 2026
d863e77
Further AI suggestions
markstockhausen Feb 17, 2026
846fd3d
Merge branch 'main' into feature/implement-scheduling-service
markstockhausen Feb 17, 2026
7916901
feat: add dashboard with contribution heatmap, badges, and session su…
markstockhausen Feb 17, 2026
4b03244
style: fix Prettier formatting
markstockhausen Feb 17, 2026
2190551
Update heatmap
markstockhausen Feb 24, 2026
785d9fa
Conform to Prettier formatting
markstockhausen Feb 24, 2026
2704820
Fix security audit
markstockhausen Feb 24, 2026
32ccd39
feat: Add initial version of onboarding
markstockhausen Feb 25, 2026
99f89e1
fix onboarding flow
markstockhausen Feb 25, 2026
4a8eeba
conform to Prettier formatting
markstockhausen Feb 25, 2026
49e98c2
Update profile setup
markstockhausen Feb 27, 2026
c4a25a2
add simple auth implementation
vtotalova Feb 27, 2026
cf2faae
Merge branch 'main' into feature/36-security-implement-secure-authent…
vtotalova Feb 27, 2026
513e8c4
Fix skip feature, update package-lock.json
markstockhausen Mar 2, 2026
47042eb
Add milestone celebration toasts during mapping sessions
markstockhausen Mar 3, 2026
23c8d8b
Update screenshot on main page, extract CompetencyNetworkViz into reu…
markstockhausen Mar 3, 2026
65c98b0
implement auth
vtotalova Mar 4, 2026
f4f7497
fix issues
vtotalova Mar 4, 2026
7303eea
fix issues
vtotalova Mar 4, 2026
2d85326
Merge remote-tracking branch 'origin/main' into feature/onboarding
markstockhausen Mar 4, 2026
f1de300
Merge branch 'main' into feature/36-security-implement-secure-authent…
vtotalova Mar 4, 2026
f3de42b
Merge branch 'main' into feature/36-security-implement-secure-authent…
vtotalova Mar 4, 2026
6f48404
Fix package-lock.json
markstockhausen Mar 4, 2026
333175a
Merge branch 'main' into feature/onboarding
markstockhausen Mar 4, 2026
305892f
Conform to Prettier formatting
markstockhausen Mar 4, 2026
ed96f32
Merge branch 'feature/onboarding' of https://github.com/ls1intum/memo…
markstockhausen Mar 4, 2026
44e8467
Fix package-lock.json
markstockhausen Mar 4, 2026
0cb944d
Fix package-lock.json
markstockhausen Mar 4, 2026
3d6d590
fix merging of main issues
vtotalova Mar 4, 2026
f05a802
Implement AI suggestions
markstockhausen Mar 4, 2026
5c0dcff
Merge branch 'feature/onboarding' into feature/36-security-implement-…
vtotalova Mar 4, 2026
6b55740
fix auth process after merging
vtotalova Mar 4, 2026
15c09ab
implement auth in the onboarding flow
vtotalova Mar 4, 2026
93fb29b
Merge branch 'main' into feature/36-security-implement-secure-authent…
vtotalova Mar 4, 2026
a072b81
fix merge conflict issues
vtotalova Mar 4, 2026
781c6fe
update onboarding with policy link
vtotalova Mar 4, 2026
72bebe5
add fast fail if login email not authorized
vtotalova Mar 4, 2026
1fc4738
remove duplicates
vtotalova Mar 4, 2026
32b1769
fix warnings
vtotalova Mar 23, 2026
d37c5d8
fix quality issues in package
vtotalova Mar 23, 2026
fd6dc02
add admin page
vtotalova Mar 24, 2026
172c52a
refine admin panel
vtotalova Apr 7, 2026
db5fbd2
Merge branch 'main' into feature/40-feat-implement-role-based-access-…
vtotalova Apr 7, 2026
6290d0a
fix npm packeges
vtotalova Apr 7, 2026
75cca56
add night mode support
vtotalova Apr 7, 2026
de0a1f8
fix formating
vtotalova Apr 7, 2026
246cf78
Merge branch 'main' into feature/40-feat-implement-role-based-access-…
vtotalova Apr 14, 2026
e55c739
fix npm packages
vtotalova Apr 14, 2026
95ab67e
integrate copilot feedback
vtotalova Apr 14, 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
32 changes: 32 additions & 0 deletions auth-flow.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
sequenceDiagram
participant B as Browser
participant KC as Keycloak
participant Mail as Email (Mailpit dev)
participant API as Spring Boot
participant DB as PostgreSQL

B->>KC: login() redirect + PKCE
KC-->>B: login / register form

alt First-time registration
B->>KC: submit university email + password
KC->>Mail: send verification email
B->>KC: click verification link
else Returning user
B->>KC: submit credentials
end

KC-->>B: JWT access token

B->>API: GET /api/auth/me + Bearer JWT
API->>KC: validate via JWK Set
API->>API: check email domain in allowlist

alt Domain not allowed
API-->>B: 403 Forbidden
B->>B: domainError = true (Access denied)
else Domain allowed
API->>DB: findOrCreate user by sub
API-->>B: {id, role}
B->>B: isAuthenticated = true
end
1 change: 1 addition & 0 deletions auth-flow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 0 additions & 5 deletions postcss.config.mjs

This file was deleted.

8 changes: 8 additions & 0 deletions public/silent-check-sso.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!doctype html>
<html>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>
1 change: 1 addition & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies {
implementation 'org.flywaydb:flyway-database-postgresql'

// Security
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'com.nimbusds:nimbus-jose-jwt:10.6'

// API Documentation
Expand Down
11 changes: 10 additions & 1 deletion server/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ services:
networks:
- memo-network

mailpit:
image: axllent/mailpit:latest
container_name: memo-mailpit
ports:
- '1025:1025'
- '8025:8025'
networks:
- memo-network

keycloak:
image: quay.io/keycloak/keycloak:26.4
container_name: memo-keycloak
Expand Down Expand Up @@ -83,7 +92,7 @@ services:
DB_NAME: memo
DB_USER: postgres
DB_PASSWORD: memo
KEYCLOAK_ISSUER_URI: http://keycloak:8080/realms/memo
KEYCLOAK_ISSUER_URI: http://localhost:8081/realms/memo
Comment thread
vtotalova marked this conversation as resolved.
KEYCLOAK_JWK_SET_URI: http://keycloak:8080/realms/memo/protocol/openid-connect/certs
SPRING_PROFILES_ACTIVE: local
ports:
Expand Down
62 changes: 29 additions & 33 deletions server/docker/keycloak/memo-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,23 @@
"attributes": {
"access.token.lifespan": "3600",
"post.logout.redirect.uris": "+"
}
},
"protocolMappers": [
{
"name": "email",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "email",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email",
"jsonType.label": "String"
}
}
]
}
],
"roles": {
Expand All @@ -38,36 +54,16 @@
}
]
},
"users": [
{
"username": "demo",
"email": "demo@memo.local",
"enabled": true,
"firstName": "Demo",
"lastName": "User",
"credentials": [
{
"type": "password",
"value": "demo",
"temporary": false
}
],
"realmRoles": ["USER"]
},
{
"username": "admin",
"email": "admin@memo.local",
"enabled": true,
"firstName": "Admin",
"lastName": "User",
"credentials": [
{
"type": "password",
"value": "admin",
"temporary": false
}
],
"realmRoles": ["ADMIN", "USER"]
}
]
"identityProviders": [],
"identityProviderMappers": [],
"verifyEmail": true,
"smtpServer": {
"host": "mailpit",
"port": "1025",
"from": "noreply@memo.local",
"fromDisplayName": "Memo",
"ssl": "false",
"starttls": "false",
"auth": "false"
}
}
2 changes: 2 additions & 0 deletions server/src/main/java/de/tum/cit/memo/MemoApiApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan
public class MemoApiApplication {

public static void main(String[] args) {
Expand Down
18 changes: 18 additions & 0 deletions server/src/main/java/de/tum/cit/memo/config/MemoProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package de.tum.cit.memo.config;

import java.util.List;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "memo")
@Getter
@Setter
public class MemoProperties {

/**
* Allowed email domains for user access. If empty, all domains are permitted.
* Example: mytum.de, in.tum.de, tum.de
*/
private List<String> allowedEmailDomains = List.of();
}
104 changes: 104 additions & 0 deletions server/src/main/java/de/tum/cit/memo/controller/AdminController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package de.tum.cit.memo.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import de.tum.cit.memo.dto.CompetencyImportRow;
import de.tum.cit.memo.dto.ImportResult;
import de.tum.cit.memo.service.CompetencyService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/api/admin")
@RequiredArgsConstructor
@Tag(name = "Admin", description = "Admin-only operations")
@SecurityRequirement(name = "bearer-jwt")
@SuppressWarnings("null")
public class AdminController {

private final CompetencyService competencyService;
private final ObjectMapper objectMapper;

@PostMapping("/competencies/import")
@Operation(summary = "Bulk import competencies from JSON array")
public ResponseEntity<ImportResult> importJson(
@Valid @RequestBody List<CompetencyImportRow> rows
) {
return ResponseEntity.ok(competencyService.bulkImportCompetencies(rows));
}

@PostMapping(value = "/competencies/import/file", consumes = "multipart/form-data")
@Operation(summary = "Bulk import competencies from uploaded CSV or JSON file")
public ResponseEntity<ImportResult> importFile(
@RequestParam("file") MultipartFile file
) throws IOException {
String originalFilename = file.getOriginalFilename();
List<CompetencyImportRow> rows;
if (originalFilename != null && originalFilename.endsWith(".csv")) {
rows = parseCsv(file);
} else {
rows = parseJson(file);
}
return ResponseEntity.ok(competencyService.bulkImportCompetencies(rows));
}

private List<CompetencyImportRow> parseCsv(MultipartFile file) throws IOException {
List<CompetencyImportRow> rows = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
String header = reader.readLine();
if (header == null) {
return rows;
}
String line;
while ((line = reader.readLine()) != null) {
String[] parts = splitCsvLine(line);
String title = parts.length > 0 ? parts[0].trim() : "";
String description = parts.length > 1 ? parts[1].trim() : null;
rows.add(new CompetencyImportRow(title, description));
}
}
return rows;
}

private String[] splitCsvLine(String line) {
List<String> fields = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inQuotes = false;
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '"') {
inQuotes = !inQuotes;
} else if (c == ',' && !inQuotes) {
fields.add(current.toString());
current = new StringBuilder();
} else {
current.append(c);
}
}
fields.add(current.toString());
return fields.toArray(new String[0]);
}
Comment thread
vtotalova marked this conversation as resolved.
Outdated

private List<CompetencyImportRow> parseJson(MultipartFile file) throws IOException {
return objectMapper.readValue(
file.getInputStream(),
objectMapper.getTypeFactory().constructCollectionType(List.class, CompetencyImportRow.class)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package de.tum.cit.memo.controller;

import de.tum.cit.memo.config.MemoProperties;
import de.tum.cit.memo.entity.User;
import de.tum.cit.memo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Tag(name = "Auth", description = "Authentication and user sync")
@SecurityRequirement(name = "bearer-jwt")
public class AuthController {

private final UserService userService;

private final MemoProperties memoProperties;

@GetMapping("/me")
@Operation(summary = "Get or create current authenticated user")
public ResponseEntity<Map<String, String>> me(@AuthenticationPrincipal Jwt jwt) {
List<String> allowedDomains = memoProperties.getAllowedEmailDomains();
if (!allowedDomains.isEmpty()) {
String email = jwt.getClaimAsString("email");
if (email == null || !isAllowedDomain(email, allowedDomains)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "Email domain not permitted. Please use a university email address."));
}
}
String sub = jwt.getSubject();
User user = userService.findOrCreateBySubject(sub);
return ResponseEntity.ok(Map.of(
"id", user.getId(),
"role", user.getRole().name()
));
}

private static boolean isAllowedDomain(String email, List<String> allowedDomains) {
int atIndex = email.indexOf('@');
if (atIndex < 0) {
return false;
}
String domain = email.substring(atIndex + 1).toLowerCase(java.util.Locale.ROOT);
return allowedDomains.contains(domain);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package de.tum.cit.memo.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CompetencyImportRow {

@NotBlank(message = "Title is required")
private String title;

private String description;
}
5 changes: 5 additions & 0 deletions server/src/main/java/de/tum/cit/memo/dto/ImportResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.tum.cit.memo.dto;

import java.util.List;

public record ImportResult(int imported, int skipped, List<String> errors) {}
Loading
Loading