Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f2280f2
Add username and password fields to StartAnalysis component
JanaNF Nov 22, 2025
77bd888
Remove log following and shutdown handling from docker_start.sh
JanaNF Nov 22, 2025
33b7407
Remove application.yml configuration file
JanaNF Nov 22, 2025
e852fb0
Implement credential-based login functionality with JWT authentication
JanaNF Nov 22, 2025
3236872
Implement dynamic authentication and repository fetching with encrypt…
JanaNF Nov 23, 2025
df7eef0
Refactor data transformation comments and add start analysis flow dia…
JanaNF Nov 23, 2025
a68c450
Refactor authentication flow in start_analysis_flow diagram to includ…
JanaNF Nov 23, 2025
1078750
Remove application.yml from .gitignore and add configuration file for…
JanaNF Nov 24, 2025
863873d
chore: update OpenAPI spec and generated client
github-actions[bot] Nov 24, 2025
3895c81
Refactor authentication and encryption logic; update API calls to rem…
JanaNF Nov 24, 2025
82e023e
Merge branch 'feature/credential-login' of https://github.com/ls1intu…
JanaNF Nov 24, 2025
dd6f1be
feat: add validation to LoginRequestDTO and simplify AuthController l…
JanaNF Nov 24, 2025
1c93849
refactor: clean up code formatting and remove unused imports in vario…
JanaNF Nov 24, 2025
750c651
chore: update OpenAPI spec and generated client
github-actions[bot] Nov 24, 2025
d268969
Remove accidentally pushed team repos and add Projects/ to gitignore
JanaNF Nov 29, 2025
d87624d
fix: remove optional parameters from onStart function in StartAnalysi…
JanaNF Nov 29, 2025
18eb5b6
feat: implement AuthResource for user authentication and cookie manag…
JanaNF Nov 29, 2025
8781966
feat: introduce GitOperationException for improved error handling in …
JanaNF Nov 29, 2025
56cce72
fix: remove redundant exception rethrow in authenticate method
JanaNF Nov 29, 2025
32aea4c
feat: refactor authentication handling to use ArtemisCredentials DTO …
JanaNF Nov 29, 2025
8563039
feat: add TestCredentialsLoader for dynamic authentication and manage…
JanaNF Nov 29, 2025
583439c
Merge main and resolve conflicts in dataLoaders.ts
JanaNF Nov 29, 2025
1943d1b
fix: update TODO comments to use 'server' instead of 'backend' for co…
JanaNF Nov 29, 2025
f69b5ac
chore: update OpenAPI spec and generated client
github-actions[bot] Nov 29, 2025
24ceefd
fix: remove trailing whitespace in ArtemisCredentials methods for cle…
JanaNF Nov 29, 2025
b27abe8
Merge branch 'feature/credential-login' of https://github.com/ls1intu…
JanaNF Nov 29, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ out/

### VS Code ###
.vscode/

Projects/
test-credentials.properties
55 changes: 55 additions & 0 deletions docs/architecture/start_analysis_flow.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@startuml StartAnalysisFlow
title Start Analysis Flow

actor User
participant "Client (React)" as Client
participant "AuthController" as Auth
participant "RequestResource" as Controller
participant "RequestService" as ReqService
participant "RepositoryFetchingService" as RepoService
participant "ArtemisClientService" as ArtemisClient
participant "GitOperationsService" as GitService
participant "Artemis Server" as Artemis
participant "Git Server" as Git

== Authentication Phase ==
User -> Client: Enter Credentials & Click "Start"
Client -> Auth: POST /api/auth/login
note right of Client: Sends username, password, serverUrl
Auth -> ArtemisClient: authenticate(url, user, pass)
ArtemisClient -> Artemis: POST /api/core/public/authenticate
Artemis --> ArtemisClient: 200 OK (Set-Cookie: jwt)
ArtemisClient --> Auth: jwtToken
Auth -> Client: 200 OK (Set-Cookie: jwt, username, password)
Client -> Client: Navigate to /teams

== Data Loading Phase ==
Client -> Controller: GET /api/requestResource/fetchAndCloneRepositories
note right of Client: Cookies included automatically
Controller -> Controller: Extract & Decrypt Credentials
Controller -> ReqService: fetchAndCloneRepositories(url, jwt, user, pass)
ReqService -> RepoService: fetchAndCloneRepositories(url, jwt, user, pass)

== Fetching Participations ==
RepoService -> ArtemisClient: fetchParticipations(url, jwt)
ArtemisClient -> Artemis: GET /api/exercise/exercises/{id}/participations
Artemis --> ArtemisClient: List<ParticipationDTO>
ArtemisClient --> RepoService: List<ParticipationDTO>

== Cloning Repositories ==
loop For each Participation
RepoService -> GitService: cloneOrPullRepository(uri, teamName, user, pass)
GitService -> Git: git clone / git pull
note right of GitService: Uses Instructor Username/Password directly
Git --> GitService: Repository Content
GitService --> RepoService: Local Path
end

== Response Phase ==
RepoService --> ReqService: List<TeamRepositoryDTO>
ReqService --> Controller: List<TeamRepositoryDTO>
Controller --> Client: 200 OK (JSON List)
Client -> Client: Transform Data (Mock Analysis)
Client -> User: Display Team Cards

@enduml
35 changes: 35 additions & 0 deletions openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,38 @@ info: {title: OpenAPI definition, version: v0}
servers:
- {url: 'http://localhost:8080', description: Generated server url}
paths:
/api/auth/login:
post:
tags: [auth-resource]
operationId: login
requestBody:
content:
application/json:
schema: {$ref: '#/components/schemas/LoginRequestDTO'}
required: true
responses:
'200': {description: OK}
/api/requestResource/fetchAndCloneRepositories:
get:
tags: [request-resource]
operationId: fetchAndCloneRepositories
parameters:
- name: jwt
in: cookie
required: false
schema: {type: string}
- name: artemis_server_url
in: cookie
required: false
schema: {type: string}
- name: artemis_username
in: cookie
required: false
schema: {type: string}
- name: artemis_password
in: cookie
required: false
schema: {type: string}
responses:
'200':
description: OK
Expand All @@ -17,6 +45,13 @@ paths:
items: {$ref: '#/components/schemas/TeamRepositoryDTO'}
components:
schemas:
LoginRequestDTO:
type: object
properties:
password: {type: string, minLength: 1}
serverUrl: {type: string, minLength: 1}
username: {type: string, minLength: 1}
required: [password, serverUrl, username]
ParticipantDTO:
type: object
properties:
Expand Down
6 changes: 0 additions & 6 deletions scripts/docker_start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,4 @@ echo -e "\n${YELLOW}Useful commands:${NC}"
echo -e "${YELLOW}• View logs:${NC} docker compose -f docker/docker-compose.yml logs -f"
echo -e "${YELLOW}• Stop services:${NC} docker compose -f docker/docker-compose.yml down"
echo -e "${YELLOW}• Restart services:${NC} docker compose -f docker/docker-compose.yml restart"
echo -e "\n${YELLOW}Press Ctrl+C to stop all services${NC}\n"

# Keep script running and handle Ctrl+C
trap "echo -e '\n${RED}Shutting down all services...${NC}'; docker compose -f docker/docker-compose.yml down; exit" INT

# Follow logs to keep script running
docker compose -f docker/docker-compose.yml logs -f
4 changes: 0 additions & 4 deletions src/main/java/de/tum/cit/aet/core/config/ArtemisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
@Setter
public class ArtemisConfig {

private String baseUrl;
private String username;
private String password;
private String jwtToken;
private Long exerciseId;
private String gitRepoPath;
private Integer numThreads;
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/de/tum/cit/aet/core/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http,
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api-docs", "/api-docs.yaml", "/actuator/health")
.requestMatchers("/api-docs", "/api-docs.yaml", "/actuator/health", "/api/auth/**")
.permitAll()
.anyRequest().authenticated()
)
.httpBasic(_ -> {});
.httpBasic(basic -> {});
return http.build();
}

Expand Down
35 changes: 35 additions & 0 deletions src/main/java/de/tum/cit/aet/core/dto/ArtemisCredentials.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package de.tum.cit.aet.core.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

/**
* DTO holding Artemis authentication credentials.
* Used to pass credentials through the application layers cleanly.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record ArtemisCredentials(
String serverUrl,
String jwtToken,
String username,
String password
) {
/**
* Checks if the credentials contain valid authentication data.
*
* @return true if serverUrl and jwtToken are present
*/
public boolean isValid() {
return serverUrl != null && !serverUrl.isBlank()
&& jwtToken != null && !jwtToken.isBlank();
}

/**
* Checks if username/password credentials are available for Git operations.
*
* @return true if both username and password are present
*/
public boolean hasGitCredentials() {
return username != null && !username.isBlank()
&& password != null && !password.isBlank();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package de.tum.cit.aet.core.exceptions;

/**
* Custom exception for errors occurring during Git operations.
* This class provides a more specific way to catch and handle failures
* related to cloning, pulling, or other Git functionality.
*/
public class GitOperationException extends RuntimeException {

/**
* Constructs a new GitOperationException with the specified detail message.
*
* @param message the detail message.
*/
public GitOperationException(String message) {
super(message);
}

/**
* Constructs a new GitOperationException with the specified detail message and cause.
*
* @param message the detail message.
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
*/
public GitOperationException(String message, Throwable cause) {
super(message, cause);
}
}
82 changes: 82 additions & 0 deletions src/main/java/de/tum/cit/aet/core/security/CryptoService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package de.tum.cit.aet.core.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;

@Service
public class CryptoService {

private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128;
private static final int GCM_IV_LENGTH = 12;

private final SecretKeySpec secretKey;

public CryptoService(@Value("${harmonia.security.secret-key:default-secret-key-change-me-in-prod}") String secret) {
this.secretKey = generateKey(secret);
}

private SecretKeySpec generateKey(String myKey) {
try {
byte[] key = myKey.getBytes(StandardCharsets.UTF_8);
MessageDigest sha = MessageDigest.getInstance("SHA-256");
key = sha.digest(key);
key = Arrays.copyOf(key, 16); // Use only first 128 bit
return new SecretKeySpec(key, "AES");
} catch (Exception e) {
throw new RuntimeException("Error generating security key", e);
}
}

/**
* Encrypts a string using AES encryption.
*
* @param strToEncrypt The string to encrypt
* @return The encrypted string (Base64 encoded)
*/
public String encrypt(String strToEncrypt) {
try {
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
byte[] cipherText = cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8));
byte[] encrypted = new byte[iv.length + cipherText.length];
System.arraycopy(iv, 0, encrypted, 0, iv.length);
System.arraycopy(cipherText, 0, encrypted, iv.length, cipherText.length);
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("Error while encrypting", e);
}
}

/**
* Decrypts a string using AES encryption.
*
* @param strToDecrypt The string to decrypt (Base64 encoded)
* @return The decrypted string
*/
public String decrypt(String strToDecrypt) {
try {
byte[] decoded = Base64.getDecoder().decode(strToDecrypt);
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(decoded, 0, iv, 0, iv.length);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
return new String(cipher.doFinal(decoded, GCM_IV_LENGTH, decoded.length - GCM_IV_LENGTH));
} catch (Exception e) {
throw new RuntimeException("Error while decrypting", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.tum.cit.aet.dataProcessing.service;

import de.tum.cit.aet.core.dto.ArtemisCredentials;
import de.tum.cit.aet.repositoryProcessing.dto.TeamRepositoryDTO;
import de.tum.cit.aet.repositoryProcessing.service.RepositoryFetchingService;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -20,12 +21,13 @@ public RequestService(RepositoryFetchingService repositoryFetchingService) {
}

/**
* Fetches and clones all repositories from Artemis.
* Fetches and clones all repositories from Artemis using dynamic credentials.
*
* @param credentials The Artemis credentials
* @return List of TeamRepositoryDTO containing repository information
*/
public List<TeamRepositoryDTO> fetchAndCloneRepositories() {
public List<TeamRepositoryDTO> fetchAndCloneRepositories(ArtemisCredentials credentials) {
log.info("RequestService: Initiating repository fetch and clone process");
return repositoryFetchingService.fetchAndCloneRepositories();
return repositoryFetchingService.fetchAndCloneRepositories(credentials);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package de.tum.cit.aet.dataProcessing.web;

import de.tum.cit.aet.core.dto.ArtemisCredentials;
import de.tum.cit.aet.core.security.CryptoService;
import de.tum.cit.aet.dataProcessing.service.RequestService;
import de.tum.cit.aet.repositoryProcessing.dto.TeamRepositoryDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -17,23 +20,55 @@
public class RequestResource {

private final RequestService requestService;
private final CryptoService cryptoService;

@Autowired
public RequestResource(RequestService requestService) {
public RequestResource(RequestService requestService, CryptoService cryptoService) {
this.requestService = requestService;
this.cryptoService = cryptoService;
}

/**
* GET endpoint to fetch and clone all repositories.
* Triggers the fetching of participations from Artemis and clones/pulls all repositories.
*
* @param jwtToken The JWT token from the cookie
* @param serverUrl The Artemis server URL from the cookie
* @param username The Artemis username from the cookie
* @param encryptedPassword The encrypted Artemis password from the cookie
* @return ResponseEntity containing the list of TeamRepositoryDTO
*/
@GetMapping("fetchAndCloneRepositories")
public ResponseEntity<List<TeamRepositoryDTO>> fetchAndCloneRepositories() {
public ResponseEntity<List<TeamRepositoryDTO>> fetchAndCloneRepositories(
@CookieValue(value = "jwt", required = false) String jwtToken,
@CookieValue(value = "artemis_server_url", required = false) String serverUrl,
@CookieValue(value = "artemis_username", required = false) String username,
@CookieValue(value = "artemis_password", required = false) String encryptedPassword
) {
log.info("GET request received: fetchAndCloneRepositories");
List<TeamRepositoryDTO> repositories = requestService.fetchAndCloneRepositories();

String password = decryptPassword(encryptedPassword);
ArtemisCredentials credentials = new ArtemisCredentials(serverUrl, jwtToken, username, password);

if (!credentials.isValid()) {
log.warn("No credentials found in cookies. Authentication required.");
return ResponseEntity.status(401).build();
}

List<TeamRepositoryDTO> repositories = requestService.fetchAndCloneRepositories(credentials);
log.info("Successfully fetched and cloned {} repositories", repositories.size());
return ResponseEntity.ok(repositories);
}

private String decryptPassword(String encryptedPassword) {
if (encryptedPassword == null) {
return null;
}
try {
return cryptoService.decrypt(encryptedPassword);
} catch (Exception e) {
log.error("Failed to decrypt password from cookie", e);
return null;
}
}
}
Loading