diff --git a/backend/pom.xml b/backend/pom.xml
index 0f95b9c..cbf1797 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -40,7 +40,7 @@
org.springframework.boot
- spring-boot-starter-security-oauth2-resource-server
+ spring-boot-starter-oauth2-client
org.springframework.boot
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/HelloController.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/HelloController.java
index eb769b2..f0caa0c 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/HelloController.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/HelloController.java
@@ -1,7 +1,7 @@
package fr.nessar.lmnporganizer.backend;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
-import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -9,10 +9,13 @@ import org.springframework.web.bind.annotation.RestController;
public class HelloController {
@GetMapping("/api/hello")
- public String hello(@AuthenticationPrincipal Jwt jwt) {
- String name = jwt.getClaimAsString("preferred_username");
+ public String hello(@AuthenticationPrincipal OAuth2User user) {
+ String name = user.getAttribute("preferred_username");
if (name == null) {
- name = jwt.getSubject();
+ name = user.getAttribute("name");
+ }
+ if (name == null) {
+ name = user.getName();
}
return "Hello, " + name + "!";
}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/SecurityConfig.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/SecurityConfig.java
index df868d9..280032c 100644
--- a/backend/src/main/java/fr/nessar/lmnporganizer/backend/SecurityConfig.java
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/SecurityConfig.java
@@ -2,9 +2,13 @@ package fr.nessar.lmnporganizer.backend;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.HttpStatusEntryPoint;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
@Configuration
@EnableWebSecurity
@@ -16,11 +20,23 @@ public class SecurityConfig {
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/hello").authenticated()
.requestMatchers("/api/apartments", "/api/apartments/**").authenticated()
+ .requestMatchers("/api/user").permitAll()
.anyRequest().permitAll()
)
- .oauth2ResourceServer(oauth2 -> oauth2
- .jwt(jwt -> {})
- );
+ .exceptionHandling(exceptions -> exceptions
+ .defaultAuthenticationEntryPointFor(
+ new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
+ PathPatternRequestMatcher.pathPattern("/api/**")
+ )
+ )
+ .oauth2Login(oauth2 -> oauth2
+ .defaultSuccessUrl("/", true)
+ )
+ .logout(logout -> logout
+ .logoutRequestMatcher(PathPatternRequestMatcher.pathPattern(HttpMethod.GET, "/logout"))
+ .logoutSuccessUrl("/")
+ )
+ .csrf(csrf -> csrf.disable());
return http.build();
}
}
diff --git a/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/UserController.java b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/UserController.java
new file mode 100644
index 0000000..f4e7804
--- /dev/null
+++ b/backend/src/main/java/fr/nessar/lmnporganizer/backend/controller/UserController.java
@@ -0,0 +1,26 @@
+package fr.nessar.lmnporganizer.backend.controller;
+
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api")
+public class UserController {
+
+ @GetMapping("/user")
+ public Map getUser(@AuthenticationPrincipal OidcUser principal) {
+ if (principal == null) {
+ return Map.of("authenticated", false);
+ }
+ return Map.of(
+ "authenticated", true,
+ "username", principal.getPreferredUsername() != null ? principal.getPreferredUsername() : principal.getName(),
+ "email", principal.getEmail() != null ? principal.getEmail() : ""
+ );
+ }
+}
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
index c203e96..0f05be9 100644
--- a/backend/src/main/resources/application.properties
+++ b/backend/src/main/resources/application.properties
@@ -1,3 +1,13 @@
spring.application.name=backend
spring.mongodb.uri=${SPRING_DATA_MONGODB_URI:mongodb://localhost:27017/lmnp}
-spring.security.oauth2.resourceserver.jwt.issuer-uri=${OIDC_ISSUER_URI:http://localhost:8090/realms/voidAuth}
+spring.security.oauth2.client.registration.oidc.client-id=${OIDC_CLIENT_ID:lmnp-client}
+spring.security.oauth2.client.registration.oidc.client-secret=${OIDC_CLIENT_SECRET:lmnp-secret}
+spring.security.oauth2.client.registration.oidc.scope=openid,profile,email
+spring.security.oauth2.client.registration.oidc.authorization-grant-type=authorization_code
+spring.security.oauth2.client.registration.oidc.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
+
+spring.security.oauth2.client.provider.oidc.authorization-uri=${OIDC_ISSUER_URI:http://localhost:8090/realms/voidAuth}/protocol/openid-connect/auth
+spring.security.oauth2.client.provider.oidc.token-uri=${OIDC_ISSUER_URI:http://localhost:8090/realms/voidAuth}/protocol/openid-connect/token
+spring.security.oauth2.client.provider.oidc.user-info-uri=${OIDC_ISSUER_URI:http://localhost:8090/realms/voidAuth}/protocol/openid-connect/userinfo
+spring.security.oauth2.client.provider.oidc.jwk-set-uri=${OIDC_ISSUER_URI:http://localhost:8090/realms/voidAuth}/protocol/openid-connect/certs
+spring.security.oauth2.client.provider.oidc.user-name-attribute=preferred_username
diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/backend/HelloControllerTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/backend/HelloControllerTest.java
index 5d56716..508a684 100644
--- a/backend/src/test/java/fr/nessar/lmnporganizer/backend/HelloControllerTest.java
+++ b/backend/src/test/java/fr/nessar/lmnporganizer/backend/HelloControllerTest.java
@@ -6,14 +6,11 @@ import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
-import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-import org.springframework.security.oauth2.jwt.JwtDecoder;
-import org.springframework.test.context.bean.override.mockito.MockitoBean;
-
@SpringBootTest(properties = "lmnp.data-initializer.enabled=false")
@AutoConfigureMockMvc
class HelloControllerTest {
@@ -21,9 +18,6 @@ class HelloControllerTest {
@Autowired
private MockMvc mockMvc;
- @MockitoBean
- private JwtDecoder jwtDecoder;
-
@Test
void whenRequestHelloWithoutToken_thenReturns401() throws Exception {
mockMvc.perform(get("/api/hello"))
@@ -33,7 +27,7 @@ class HelloControllerTest {
@Test
void whenRequestHelloWithToken_thenReturnsGreeting() throws Exception {
mockMvc.perform(get("/api/hello")
- .with(jwt().jwt(builder -> builder.claim("preferred_username", "John Doe"))))
+ .with(oidcLogin().idToken(token -> token.claim("preferred_username", "John Doe"))))
.andExpect(status().isOk())
.andExpect(content().string("Hello, John Doe!"));
}
diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/ApartmentControllerTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/ApartmentControllerTest.java
index 815166a..ed89219 100644
--- a/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/ApartmentControllerTest.java
+++ b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/ApartmentControllerTest.java
@@ -7,7 +7,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
-import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
@@ -17,7 +16,7 @@ import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
-import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -32,9 +31,6 @@ class ApartmentControllerTest {
@MockitoBean
private ApartmentRepository apartmentRepository;
- @MockitoBean
- private JwtDecoder jwtDecoder;
-
@Test
void whenGetApartmentsWithoutToken_thenUnauthorized() throws Exception {
mockMvc.perform(get("/api/apartments"))
@@ -46,7 +42,7 @@ class ApartmentControllerTest {
Apartment apt = new Apartment("1", "default-landlord", "Appartement 1", "Adresse 1", 150000.0, "Desc");
when(apartmentRepository.findAll()).thenReturn(List.of(apt));
- mockMvc.perform(get("/api/apartments").with(jwt()))
+ mockMvc.perform(get("/api/apartments").with(oidcLogin()))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").value("1"))
.andExpect(jsonPath("$[0].name").value("Appartement 1"));
@@ -56,7 +52,7 @@ class ApartmentControllerTest {
void whenGetApartmentByIdNotFound_thenNotFound() throws Exception {
when(apartmentRepository.findById("999")).thenReturn(Optional.empty());
- mockMvc.perform(get("/api/apartments/999").with(jwt()))
+ mockMvc.perform(get("/api/apartments/999").with(oidcLogin()))
.andExpect(status().isNotFound());
}
@@ -65,7 +61,7 @@ class ApartmentControllerTest {
Apartment apt = new Apartment("1", "default-landlord", "Appartement 1", "Adresse 1", 150000.0, "Desc");
when(apartmentRepository.findById("1")).thenReturn(Optional.of(apt));
- mockMvc.perform(get("/api/apartments/1").with(jwt()))
+ mockMvc.perform(get("/api/apartments/1").with(oidcLogin()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value("1"));
}
@@ -76,7 +72,7 @@ class ApartmentControllerTest {
when(apartmentRepository.save(any(Apartment.class))).thenReturn(savedApt);
mockMvc.perform(post("/api/apartments")
- .with(jwt())
+ .with(oidcLogin())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"New Apartment\",\"address\":\"Address\",\"purchasePrice\":200000.0,\"description\":\"Desc\"}"))
.andExpect(status().isCreated())
@@ -89,7 +85,7 @@ class ApartmentControllerTest {
when(apartmentRepository.findById("999")).thenReturn(Optional.empty());
mockMvc.perform(put("/api/apartments/999")
- .with(jwt())
+ .with(oidcLogin())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"New Name\",\"address\":\"New Address\",\"purchasePrice\":120000.0,\"description\":\"New Desc\"}"))
.andExpect(status().isNotFound());
@@ -104,7 +100,7 @@ class ApartmentControllerTest {
when(apartmentRepository.save(any(Apartment.class))).thenReturn(updated);
mockMvc.perform(put("/api/apartments/1")
- .with(jwt())
+ .with(oidcLogin())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"New Name\",\"address\":\"New Address\",\"purchasePrice\":120000.0,\"description\":\"New Desc\"}"))
.andExpect(status().isOk())
@@ -116,7 +112,7 @@ class ApartmentControllerTest {
void whenDeleteApartmentNotFound_thenNotFound() throws Exception {
when(apartmentRepository.existsById("999")).thenReturn(false);
- mockMvc.perform(delete("/api/apartments/999").with(jwt()))
+ mockMvc.perform(delete("/api/apartments/999").with(oidcLogin()))
.andExpect(status().isNotFound());
}
@@ -125,7 +121,7 @@ class ApartmentControllerTest {
when(apartmentRepository.existsById("1")).thenReturn(true);
doNothing().when(apartmentRepository).deleteById("1");
- mockMvc.perform(delete("/api/apartments/1").with(jwt()))
+ mockMvc.perform(delete("/api/apartments/1").with(oidcLogin()))
.andExpect(status().isNoContent());
verify(apartmentRepository, times(1)).deleteById("1");
diff --git a/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/UserControllerTest.java b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/UserControllerTest.java
new file mode 100644
index 0000000..79a5146
--- /dev/null
+++ b/backend/src/test/java/fr/nessar/lmnporganizer/backend/controller/UserControllerTest.java
@@ -0,0 +1,43 @@
+package fr.nessar.lmnporganizer.backend.controller;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest(properties = "lmnp.data-initializer.enabled=false")
+@AutoConfigureMockMvc
+class UserControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ void whenGetUserUnauthenticated_thenReturnsAuthenticatedFalse() throws Exception {
+ mockMvc.perform(get("/api/user"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authenticated").value(false));
+ }
+
+ @Test
+ void whenGetUserAuthenticated_thenReturnsUserDetails() throws Exception {
+ mockMvc.perform(get("/api/user")
+ .with(oidcLogin()
+ .idToken(token -> token
+ .claim("preferred_username", "johndoe")
+ .claim("email", "john@example.com")
+ )
+ )
+ )
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.authenticated").value(true))
+ .andExpect(jsonPath("$.username").value("johndoe"))
+ .andExpect(jsonPath("$.email").value("john@example.com"));
+ }
+}
diff --git a/compose.yml b/compose.yml
index a002249..15b32f7 100644
--- a/compose.yml
+++ b/compose.yml
@@ -26,6 +26,8 @@ services:
environment:
SPRING_DATA_MONGODB_URI: mongodb://admin:adminpassword@mongodb:27017/lmnp?authSource=admin
OIDC_ISSUER_URI: ${OIDC_ISSUER_URI:-http://localhost:8090/realms/voidAuth}
+ OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-lmnp-client}
+ OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-lmnp-secret}
depends_on:
mongodb:
condition: service_healthy
@@ -34,10 +36,6 @@ services:
build:
context: ./frontend
dockerfile: Dockerfile
- args:
- - OIDC_ISSUER_URI=${OIDC_ISSUER_URI:-http://localhost:8090/realms/voidAuth}
- - OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-lmnp-client}
- - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET:-lmnp-secret}
container_name: lmnp-frontend
ports:
- "80:80"
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index d85dcf0..bb1f947 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -11,15 +11,6 @@ RUN bun install --frozen-lockfile
COPY . .
-# Accept build arguments for environment variables
-ARG OIDC_ISSUER_URI
-ARG OIDC_CLIENT_ID
-ARG OIDC_CLIENT_SECRET
-
-# Set build-time environment variables for @ngx-env/builder
-ENV OIDC_ISSUER_URI=$OIDC_ISSUER_URI
-ENV OIDC_CLIENT_ID=$OIDC_CLIENT_ID
-ENV OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET
ENV CI=true
ENV NG_CLI_ANALYTICS=false
diff --git a/frontend/angular.json b/frontend/angular.json
index 8cc12b7..20b03df 100644
--- a/frontend/angular.json
+++ b/frontend/angular.json
@@ -15,7 +15,7 @@
"prefix": "app",
"architect": {
"build": {
- "builder": "@ngx-env/builder:application",
+ "builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
@@ -25,10 +25,7 @@
"input": "public"
}
],
- "styles": ["src/styles.css"],
- "ngxEnv": {
- "prefix": "OIDC_"
- }
+ "styles": ["src/styles.css"]
},
"configurations": {
"production": {
@@ -55,7 +52,7 @@
"defaultConfiguration": "production"
},
"serve": {
- "builder": "@ngx-env/builder:dev-server",
+ "builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "lmnp-frontend:build:production"
@@ -68,10 +65,9 @@
"options": {}
},
"test": {
- "builder": "@ngx-env/builder:unit-test",
+ "builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json",
- "coverage": true,
"coverageExclude": ["**/*.html", "src/main.ts"]
}
},
diff --git a/frontend/bun.lock b/frontend/bun.lock
index 24e4ce1..8f15823 100644
--- a/frontend/bun.lock
+++ b/frontend/bun.lock
@@ -11,7 +11,6 @@
"@angular/forms": "^22.0.0",
"@angular/platform-browser": "^22.0.0",
"@angular/router": "^22.0.0",
- "angular-oauth2-oidc": "^22.0.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
},
@@ -586,8 +585,6 @@
"angular-eslint": ["angular-eslint@22.1.0", "", { "dependencies": { "@angular-devkit/core": ">= 22.0.0 < 23.0.0", "@angular-devkit/schematics": ">= 22.0.0 < 23.0.0", "@angular-eslint/builder": "22.1.0", "@angular-eslint/eslint-plugin": "22.1.0", "@angular-eslint/eslint-plugin-template": "22.1.0", "@angular-eslint/schematics": "22.1.0", "@angular-eslint/template-parser": "22.1.0", "@typescript-eslint/types": "^8.0.0", "@typescript-eslint/utils": "^8.0.0" }, "peerDependencies": { "@angular/cli": ">= 22.0.0 < 23.0.0", "eslint": "^9.0.0 || ^10.0.0", "typescript": "*", "typescript-eslint": "^8.0.0" } }, "sha512-LU5a6MOSeQTETnc4xaHi10p9S1cstNczr93sChi3rx8IzgH4+risIsjjQHVLSnQDrSGH2O8jAGCaChker7f7TQ=="],
- "angular-oauth2-oidc": ["angular-oauth2-oidc@22.0.2", "", { "dependencies": { "tslib": "^2.5.2" }, "peerDependencies": { "@angular/common": ">=22.0.0", "@angular/core": ">=22.0.0" } }, "sha512-MDzmf0quLVnsCzVp/G7Pr6YTSMoMMo62u8QFqJzM3madQmWV5JI4dMppvuTPswH+4V2gx4zPXdCjnn8ikVZX+A=="],
-
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
diff --git a/frontend/package.json b/frontend/package.json
index 5f2831f..e30bc50 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -18,7 +18,6 @@
"@angular/forms": "^22.0.0",
"@angular/platform-browser": "^22.0.0",
"@angular/router": "^22.0.0",
- "angular-oauth2-oidc": "^22.0.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
},
diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts
index 4e3c92c..0df0178 100644
--- a/frontend/src/app/app.config.ts
+++ b/frontend/src/app/app.config.ts
@@ -1,7 +1,6 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideAppInitializer, inject } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
-import { provideOAuthClient, OAuthService } from 'angular-oauth2-oidc';
import { routes } from './app.routes';
import { ConfigService } from './config.service';
@@ -11,23 +10,9 @@ export const appConfig: ApplicationConfig = {
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(),
- provideOAuthClient(),
provideAppInitializer(() => {
const configService = inject(ConfigService);
- const oauthService = inject(OAuthService);
- return configService.loadConfig().then(() => {
- oauthService.configure({
- issuer: configService.oidcIssuer,
- redirectUri: window.location.origin + '/',
- clientId: configService.clientId,
- dummyClientSecret: configService.clientSecret,
- responseType: 'code',
- scope: 'openid profile email',
- showDebugInformation: true,
- requireHttps: false
- });
- return oauthService.loadDiscoveryDocumentAndTryLogin();
- });
+ return configService.loadConfig();
})
]
};
diff --git a/frontend/src/app/app.spec.ts b/frontend/src/app/app.spec.ts
index e780d67..abaa45f 100644
--- a/frontend/src/app/app.spec.ts
+++ b/frontend/src/app/app.spec.ts
@@ -1,27 +1,27 @@
import { TestBed } from '@angular/core/testing';
import { App } from './app';
-import { OAuthService } from 'angular-oauth2-oidc';
import { HttpClient } from '@angular/common/http';
import { ApartmentService } from './apartment.service';
import { of, throwError } from 'rxjs';
import { vi } from 'vitest';
describe('App', () => {
- let mockOAuthService: any;
let mockHttpClient: any;
let mockApartmentService: any;
+ let mockUserResponse: { authenticated: boolean; username: string; email: string };
+ let originalLocation: any;
+ let locationMock: { href: string; origin: string };
beforeEach(async () => {
- mockOAuthService = {
- hasValidAccessToken: () => false,
- getIdentityClaims: () => null,
- getAccessToken: () => 'mock-token',
- initCodeFlow: vi.fn(),
- revokeTokenAndLogout: vi.fn()
- };
+ mockUserResponse = { authenticated: false, username: '', email: '' };
mockHttpClient = {
- get: () => of('Hello from mocked API!')
+ get: (url: string) => {
+ if (url === '/api/user') {
+ return of(mockUserResponse);
+ }
+ return of('Hello from mocked API!');
+ }
};
mockApartmentService = {
@@ -34,16 +34,27 @@ describe('App', () => {
deleteApartment: () => of(undefined)
};
+ originalLocation = window.location;
+ delete (window as any).location;
+ locationMock = {
+ href: '',
+ origin: 'http://localhost'
+ };
+ (window as any).location = locationMock;
+
await TestBed.configureTestingModule({
imports: [App],
providers: [
- { provide: OAuthService, useValue: mockOAuthService },
{ provide: HttpClient, useValue: mockHttpClient },
{ provide: ApartmentService, useValue: mockApartmentService }
]
}).compileComponents();
});
+ afterEach(() => {
+ (window as any).location = originalLocation;
+ });
+
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
@@ -52,51 +63,38 @@ describe('App', () => {
it('should render title', async () => {
const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
await fixture.whenStable();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('LMNP Organizer');
});
- it('should return login status based on OAuthService', () => {
+ it('should return login status based on user endpoint response', async () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
-
- mockOAuthService.hasValidAccessToken = () => true;
- expect(app.isLoggedIn).toBe(true);
- mockOAuthService.hasValidAccessToken = () => false;
+ // Initially unauthenticated
+ fixture.detectChanges();
+ await fixture.whenStable();
expect(app.isLoggedIn).toBe(false);
- });
-
- it('should return username and email from identity claims', () => {
- const fixture = TestBed.createComponent(App);
- const app = fixture.componentInstance;
-
- // Default username when no claims
- expect(app.username).toBe('');
-
- // Custom claims
- mockOAuthService.getIdentityClaims = () => ({
- preferred_username: 'Alice',
- email: 'alice@example.com'
- });
+ // Activating authenticated response and calling ngOnInit manually
+ mockUserResponse = { authenticated: true, username: 'Alice', email: 'alice@example.com' };
+ app.ngOnInit();
+ expect(app.isLoggedIn).toBe(true);
expect(app.username).toBe('Alice');
expect(app.email).toBe('alice@example.com');
});
- it('should invoke OIDC login and logout flows', () => {
+ it('should invoke OIDC login and logout flows by redirecting the browser window', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
- const loginSpy = vi.spyOn(mockOAuthService, 'initCodeFlow');
- const logoutSpy = vi.spyOn(mockOAuthService, 'revokeTokenAndLogout');
-
app.login();
- expect(loginSpy).toHaveBeenCalled();
+ expect(locationMock.href).toBe('/oauth2/authorization/oidc');
app.logout();
- expect(logoutSpy).toHaveBeenCalled();
+ expect(locationMock.href).toBe('/logout');
});
it('should call hello API and set message on success', () => {
@@ -111,22 +109,21 @@ describe('App', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
- mockHttpClient.get = () => throwError(() => ({ statusText: 'Unauthorized' }));
+ mockHttpClient.get = (url: string) => {
+ if (url === '/api/user') {
+ return of(mockUserResponse);
+ }
+ return throwError(() => ({ statusText: 'Unauthorized' }));
+ };
app.callHelloApi();
expect(app.helloMessage()).toBe('Failed to connect to backend: Unauthorized');
});
it('should render logged in state when user is authenticated', async () => {
+ mockUserResponse = { authenticated: true, username: 'Alice', email: 'alice@example.com' };
const fixture = TestBed.createComponent(App);
- // Log in
- mockOAuthService.hasValidAccessToken = () => true;
- mockOAuthService.getIdentityClaims = () => ({
- preferred_username: 'Alice',
- email: 'alice@example.com'
- });
-
fixture.detectChanges();
await fixture.whenStable();
@@ -136,16 +133,10 @@ describe('App', () => {
});
it('should render hello API response when API button is clicked', async () => {
+ mockUserResponse = { authenticated: true, username: 'Alice', email: 'alice@example.com' };
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
- // Log in
- mockOAuthService.hasValidAccessToken = () => true;
- mockOAuthService.getIdentityClaims = () => ({
- preferred_username: 'Alice',
- email: 'alice@example.com'
- });
-
fixture.detectChanges();
await fixture.whenStable();
@@ -165,7 +156,7 @@ describe('App', () => {
// Apartment CRUD tests
it('should load apartments on init if authenticated', async () => {
- mockOAuthService.hasValidAccessToken = () => true;
+ mockUserResponse = { authenticated: true, username: 'Alice', email: 'alice@example.com' };
const loadSpy = vi.spyOn(mockApartmentService, 'getApartments');
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
diff --git a/frontend/src/app/app.ts b/frontend/src/app/app.ts
index efb6ed5..7366663 100644
--- a/frontend/src/app/app.ts
+++ b/frontend/src/app/app.ts
@@ -1,6 +1,5 @@
import { Component, signal, inject, OnInit, effect } from '@angular/core';
-import { OAuthService } from 'angular-oauth2-oidc';
-import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { HttpClient } from '@angular/common/http';
import { ApartmentService, Apartment } from './apartment.service';
import { FormsModule } from '@angular/forms';
@@ -27,7 +26,10 @@ export class App implements OnInit {
formDescription = '';
formError = '';
- private readonly oauthService = inject(OAuthService);
+ private readonly _isLoggedIn = signal(false);
+ private readonly _username = signal('');
+ private readonly _email = signal('');
+
private readonly http = inject(HttpClient);
private readonly apartmentService = inject(ApartmentService);
@@ -40,40 +42,49 @@ export class App implements OnInit {
}
ngOnInit() {
- if (this.isLoggedIn) {
- this.loadApartments();
- }
+ this.http.get<{authenticated: boolean, username?: string, email?: string}>('/api/user').subscribe({
+ next: (user) => {
+ if (user.authenticated) {
+ this._isLoggedIn.set(true);
+ this._username.set(user.username || 'User');
+ this._email.set(user.email || '');
+ } else {
+ this._isLoggedIn.set(false);
+ this._username.set('');
+ this._email.set('');
+ }
+ },
+ error: (err) => {
+ console.error('Error fetching user authentication status', err);
+ this._isLoggedIn.set(false);
+ this._username.set('');
+ this._email.set('');
+ }
+ });
}
get isLoggedIn(): boolean {
- return this.oauthService.hasValidAccessToken();
+ return this._isLoggedIn();
}
get username(): string {
- const claims = this.oauthService.getIdentityClaims() as Record | null;
- if (!claims) return '';
- return claims['preferred_username'] || claims['given_name'] || claims['name'] || 'User';
+ return this._username();
}
get email(): string {
- const claims = this.oauthService.getIdentityClaims() as Record | null;
- return claims ? claims['email'] : '';
+ return this._email();
}
login() {
- this.oauthService.initCodeFlow();
+ window.location.href = '/oauth2/authorization/oidc';
}
logout() {
- this.oauthService.revokeTokenAndLogout();
+ window.location.href = '/logout';
}
callHelloApi() {
- const headers = new HttpHeaders({
- 'Authorization': 'Bearer ' + this.oauthService.getAccessToken()
- });
-
- this.http.get('/api/hello', { headers, responseType: 'text' }).subscribe({
+ this.http.get('/api/hello', { responseType: 'text' }).subscribe({
next: (msg) => this.helloMessage.set(msg),
error: (err) => {
console.error(err);
diff --git a/frontend/src/app/config.service.spec.ts b/frontend/src/app/config.service.spec.ts
index 49fd46c..7854209 100644
--- a/frontend/src/app/config.service.spec.ts
+++ b/frontend/src/app/config.service.spec.ts
@@ -1,15 +1,10 @@
import { TestBed } from '@angular/core/testing';
import { ConfigService } from './config.service';
-import { vi } from 'vitest';
describe('ConfigService', () => {
let service: ConfigService;
beforeEach(() => {
- // Reset env variables before each test
- vi.stubEnv('OIDC_ISSUER_URI', '');
- vi.stubEnv('OIDC_CLIENT_ID', '');
-
TestBed.configureTestingModule({
providers: [ConfigService]
});
@@ -21,19 +16,6 @@ describe('ConfigService', () => {
expect(service).toBeTruthy();
});
- it('should return default values when env variables are not set', () => {
- expect(service.oidcIssuer).toBe('http://localhost:8090/realms/voidAuth');
- expect(service.clientId).toBe('lmnp-client');
- });
-
- it('should return env values when env variables are set', () => {
- vi.stubEnv('OIDC_ISSUER_URI', 'http://env-issuer');
- vi.stubEnv('OIDC_CLIENT_ID', 'env-client');
-
- expect(service.oidcIssuer).toBe('http://env-issuer');
- expect(service.clientId).toBe('env-client');
- });
-
it('should resolve loadConfig successfully', async () => {
await expect(service.loadConfig()).resolves.toBeUndefined();
});
diff --git a/frontend/src/app/config.service.ts b/frontend/src/app/config.service.ts
index 9d5172c..3f9f199 100644
--- a/frontend/src/app/config.service.ts
+++ b/frontend/src/app/config.service.ts
@@ -7,16 +7,4 @@ export class ConfigService {
async loadConfig(): Promise {
return Promise.resolve();
}
-
- get oidcIssuer(): string {
- return import.meta.env['OIDC_ISSUER_URI'] || 'http://localhost:8090/realms/voidAuth';
- }
-
- get clientId(): string {
- return import.meta.env['OIDC_CLIENT_ID'] || 'lmnp-client';
- }
-
- get clientSecret(): string {
- return import.meta.env['OIDC_CLIENT_SECRET'] || 'lmnp-secret';
- }
}
diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts
deleted file mode 100644
index 7be7f8a..0000000
--- a/frontend/src/env.d.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-// Define the type of the environment variables.
-declare interface Env {
- readonly NODE_ENV: string;
- readonly OIDC_ISSUER_URI?: string;
- readonly OIDC_CLIENT_ID?: string;
- [key: string]: string | undefined;
-}
-
-// Choose how to access the environment variables.
-// Remove the unused options.
-
-// 1. Use import.meta.env.YOUR_ENV_VAR in your code. (conventional)
-declare interface ImportMeta {
- readonly env: Env;
-}
-
-// 2. Use _NGX_ENV_.YOUR_ENV_VAR in your code. (customizable)
-// You can modify the name of the variable in angular.json.
-// ngxEnv: {
-// define: '_NGX_ENV_',
-// }
-declare const _NGX_ENV_: Env;