Skip to content

Commit 6e482fa

Browse files
committed
Updated README and added documentation on Profile extension framework
1 parent 1e08c5c commit 6e482fa

File tree

2 files changed

+635
-79
lines changed

2 files changed

+635
-79
lines changed

PROFILE.md

Lines changed: 399 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,399 @@
1+
# User Profile Extension Framework
2+
3+
This guide explains how to leverage and extend the user profile system in Spring User Framework to create rich, application-specific user data models.
4+
5+
## Table of Contents
6+
- [User Profile Extension Framework](#user-profile-extension-framework)
7+
- [Table of Contents](#table-of-contents)
8+
- [Overview](#overview)
9+
- [When to Use Profile Extensions](#when-to-use-profile-extensions)
10+
- [Core Components](#core-components)
11+
- [Implementation Guide](#implementation-guide)
12+
- [Step 1: Create Your Custom User Profile](#step-1-create-your-custom-user-profile)
13+
- [Step 2: Create a Profile Repository](#step-2-create-a-profile-repository)
14+
- [Step 3: Implement a Profile Service](#step-3-implement-a-profile-service)
15+
- [Step 4: Create a Session Profile Manager](#step-4-create-a-session-profile-manager)
16+
- [Step 5: Implement an Authentication Listener](#step-5-implement-an-authentication-listener)
17+
- [Usage Examples](#usage-examples)
18+
- [Accessing Profile Data in Controllers](#accessing-profile-data-in-controllers)
19+
- [Using Profiles in Views](#using-profiles-in-views)
20+
- [Profile-Based Authorization](#profile-based-authorization)
21+
- [Advanced Customizations](#advanced-customizations)
22+
- [Custom Profile Initialization](#custom-profile-initialization)
23+
- [Additional Event Handling](#additional-event-handling)
24+
- [Profile Migration Strategies](#profile-migration-strategies)
25+
- [Troubleshooting](#troubleshooting)
26+
27+
## Overview
28+
29+
The Spring User Framework provides an extensible user profile system that allows you to:
30+
31+
1. **Store application-specific user data** beyond the core authentication details
32+
2. **Access profile information throughout the application** via session-scoped components
33+
3. **Automatically load profiles during authentication** with minimal configuration
34+
4. **Keep user-related data organized** in a type-safe, structured manner
35+
36+
This system is built on Spring's dependency injection, JPA persistence, and session management capabilities, making it seamlessly integrated with your Spring Boot application.
37+
38+
## When to Use Profile Extensions
39+
40+
Consider extending the profile system when you need to:
41+
42+
- Store user preferences, settings, or application-specific data
43+
- Track user activity or state across sessions
44+
- Associate domain-specific entities with users (e.g., subscriptions, permissions)
45+
- Implement features requiring additional user properties beyond authentication
46+
47+
If your application only needs basic authentication without user-specific data, you may not need to implement these extensions.
48+
49+
## Core Components
50+
51+
The profile extension framework consists of these key components:
52+
53+
1. **`BaseUserProfile`**: The JPA entity base class that links to the core `User` entity
54+
2. **`UserProfileService<T>`**: Interface for retrieving and managing profile objects
55+
3. **`BaseSessionProfile<T>`**: Session-scoped container that holds the current user's profile
56+
4. **`BaseAuthenticationListener<T>`**: Loads the profile on successful authentication
57+
58+
All components use generics to ensure type safety throughout your application.
59+
60+
## Implementation Guide
61+
62+
### Step 1: Create Your Custom User Profile
63+
64+
Create a JPA entity that extends `BaseUserProfile`:
65+
66+
```java
67+
@Entity
68+
@Table(name = "app_user_profile")
69+
@Data
70+
@EqualsAndHashCode(callSuper = true)
71+
public class AppUserProfile extends BaseUserProfile {
72+
// Add your application-specific fields
73+
74+
private String displayName;
75+
76+
@Enumerated(EnumType.STRING)
77+
private AccountType accountType;
78+
79+
private boolean notificationsEnabled;
80+
81+
@OneToMany(mappedBy = "userProfile", cascade = CascadeType.ALL, orphanRemoval = true)
82+
private List<UserPreference> preferences = new ArrayList<>();
83+
84+
// Domain-specific methods
85+
public void addPreference(UserPreference preference) {
86+
preferences.add(preference);
87+
preference.setUserProfile(this);
88+
}
89+
90+
public boolean hasPreference(String key) {
91+
return preferences.stream()
92+
.anyMatch(p -> p.getKey().equals(key));
93+
}
94+
}
95+
```
96+
97+
The `BaseUserProfile` class already provides:
98+
- An ID field that maps to the User ID
99+
- A one-to-one relationship with the User entity
100+
- Common fields like lastAccessed and locale
101+
102+
### Step 2: Create a Profile Repository
103+
104+
Create a repository interface for your profile entity:
105+
106+
```java
107+
public interface AppUserProfileRepository extends JpaRepository<AppUserProfile, Long> {
108+
Optional<AppUserProfile> findByUserId(Long userId);
109+
}
110+
```
111+
112+
### Step 3: Implement a Profile Service
113+
114+
Implement the `UserProfileService` interface to manage your profile entity:
115+
116+
```java
117+
@Service
118+
@Transactional
119+
@RequiredArgsConstructor
120+
public class AppUserProfileService implements UserProfileService<AppUserProfile> {
121+
122+
private final AppUserProfileRepository profileRepository;
123+
private final UserRepository userRepository;
124+
125+
@Override
126+
public AppUserProfile getOrCreateProfile(User user) {
127+
if (user == null) {
128+
throw new IllegalArgumentException("User must not be null");
129+
}
130+
131+
return profileRepository.findByUserId(user.getId())
132+
.orElseGet(() -> createAndSaveProfile(user));
133+
}
134+
135+
@Override
136+
public AppUserProfile updateProfile(AppUserProfile profile) {
137+
if (profile == null) {
138+
throw new IllegalArgumentException("Profile must not be null");
139+
}
140+
return profileRepository.save(profile);
141+
}
142+
143+
private AppUserProfile createAndSaveProfile(User user) {
144+
User managedUser = userRepository.findById(user.getId())
145+
.orElseThrow(() -> new IllegalArgumentException("User not found"));
146+
147+
AppUserProfile profile = new AppUserProfile();
148+
profile.setUser(managedUser);
149+
150+
// Set default values for new profiles
151+
profile.setDisplayName(user.getFirstName() + " " + user.getLastName());
152+
profile.setAccountType(AccountType.BASIC);
153+
profile.setNotificationsEnabled(true);
154+
155+
return profileRepository.save(profile);
156+
}
157+
158+
// Additional application-specific methods
159+
public void upgradeAccount(AppUserProfile profile, AccountType newType) {
160+
profile.setAccountType(newType);
161+
profileRepository.save(profile);
162+
}
163+
}
164+
```
165+
166+
### Step 4: Create a Session Profile Manager
167+
168+
Create a session-scoped component to access the current user's profile:
169+
170+
```java
171+
@Component
172+
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
173+
public class AppSessionProfile extends BaseSessionProfile<AppUserProfile> {
174+
175+
// Add custom accessor methods for your application
176+
public String getDisplayName() {
177+
return getUserProfile() != null ? getUserProfile().getDisplayName() : null;
178+
}
179+
180+
public boolean isNotificationsEnabled() {
181+
return getUserProfile() != null && getUserProfile().isNotificationsEnabled();
182+
}
183+
184+
public AccountType getAccountType() {
185+
return getUserProfile() != null ? getUserProfile().getAccountType() : null;
186+
}
187+
188+
public boolean isPremiumUser() {
189+
return getUserProfile() != null &&
190+
getUserProfile().getAccountType() == AccountType.PREMIUM;
191+
}
192+
}
193+
```
194+
195+
### Step 5: Implement an Authentication Listener
196+
197+
Create a listener to load profiles during authentication:
198+
199+
```java
200+
@Component
201+
public class AppAuthenticationListener extends BaseAuthenticationListener<AppUserProfile> {
202+
203+
public AppAuthenticationListener(AppSessionProfile sessionProfile,
204+
AppUserProfileService profileService) {
205+
super(sessionProfile, profileService);
206+
}
207+
208+
// Optionally override event handling methods
209+
}
210+
```
211+
212+
That's it! With these components in place, your application will automatically:
213+
1. Load the user's profile upon successful authentication
214+
2. Store the profile in the session for easy access
215+
3. Allow you to read and update profile data throughout your application
216+
217+
## Usage Examples
218+
219+
### Accessing Profile Data in Controllers
220+
221+
```java
222+
@Controller
223+
@RequiredArgsConstructor
224+
public class DashboardController {
225+
226+
private final AppSessionProfile sessionProfile;
227+
228+
@GetMapping("/dashboard")
229+
public String dashboard(Model model) {
230+
// Access profile data
231+
model.addAttribute("displayName", sessionProfile.getDisplayName());
232+
model.addAttribute("isPremium", sessionProfile.isPremiumUser());
233+
234+
// Access the underlying User object if needed
235+
User user = sessionProfile.getUser();
236+
237+
// Use the full profile object
238+
AppUserProfile profile = sessionProfile.getUserProfile();
239+
240+
return "dashboard";
241+
}
242+
}
243+
```
244+
245+
### Using Profiles in Views
246+
247+
In Thymeleaf templates, you can directly access the session profile:
248+
249+
```html
250+
<!-- With SessionProfile automatically added to model -->
251+
<div th:if="${appSessionProfile.premiumUser}">
252+
<p>Welcome, Premium Member <span th:text="${appSessionProfile.displayName}">User</span>!</p>
253+
<!-- Premium-only content -->
254+
</div>
255+
256+
<!-- Or using sec:authorize -->
257+
<div sec:authorize="@appSessionProfile.isPremiumUser()">
258+
<!-- Premium-only content -->
259+
</div>
260+
```
261+
262+
### Profile-Based Authorization
263+
264+
You can use profile data for fine-grained authorization:
265+
266+
```java
267+
@PreAuthorize("@appSessionProfile.isPremiumUser()")
268+
@GetMapping("/premium-content")
269+
public String premiumContent() {
270+
return "premium/content";
271+
}
272+
```
273+
274+
## Advanced Customizations
275+
276+
### Custom Profile Initialization
277+
278+
Override the `getOrCreateProfile` method to implement custom initialization logic:
279+
280+
```java
281+
@Override
282+
public AppUserProfile getOrCreateProfile(User user) {
283+
return profileRepository.findByUserId(user.getId())
284+
.orElseGet(() -> {
285+
AppUserProfile profile = new AppUserProfile();
286+
profile.setUser(user);
287+
288+
// Apply business logic for new profiles
289+
if (user.getEmail().endsWith("@company.com")) {
290+
profile.setAccountType(AccountType.INTERNAL);
291+
}
292+
293+
// Set up default preferences
294+
UserPreference theme = new UserPreference();
295+
theme.setKey("theme");
296+
theme.setValue("light");
297+
profile.addPreference(theme);
298+
299+
return profileRepository.save(profile);
300+
});
301+
}
302+
```
303+
304+
### Additional Event Handling
305+
306+
You can handle more authentication-related events by adding methods to your listener:
307+
308+
```java
309+
@Component
310+
public class ExtendedAuthListener extends BaseAuthenticationListener<AppUserProfile> {
311+
312+
private final LoginAttemptService loginAttemptService;
313+
314+
public ExtendedAuthListener(
315+
AppSessionProfile sessionProfile,
316+
AppUserProfileService profileService,
317+
LoginAttemptService loginAttemptService) {
318+
super(sessionProfile, profileService);
319+
this.loginAttemptService = loginAttemptService;
320+
}
321+
322+
@EventListener
323+
public void onLogoutSuccess(LogoutSuccessEvent event) {
324+
// Handle logout, e.g., update last logout timestamp
325+
if (event.getAuthentication().getPrincipal() instanceof DSUserDetails) {
326+
User user = ((DSUserDetails) event.getAuthentication().getPrincipal()).getUser();
327+
AppUserProfile profile = profileService.getOrCreateProfile(user);
328+
profile.setLastLogout(new Date());
329+
profileService.updateProfile(profile);
330+
}
331+
}
332+
}
333+
```
334+
335+
### Profile Migration Strategies
336+
337+
If you need to migrate or update existing profiles:
338+
339+
```java
340+
@Service
341+
@RequiredArgsConstructor
342+
public class ProfileMigrationService {
343+
344+
private final AppUserProfileRepository profileRepository;
345+
346+
@Transactional
347+
@Scheduled(fixedRate = 86400000) // Daily
348+
public void migrateProfilesToNewSchema() {
349+
List<AppUserProfile> profiles = profileRepository.findAll();
350+
for (AppUserProfile profile : profiles) {
351+
// Perform migration logic
352+
if (profile.getAccountType() == null) {
353+
profile.setAccountType(AccountType.BASIC);
354+
}
355+
356+
// Initialize new fields with default values
357+
if (profile.getPreferences().isEmpty()) {
358+
UserPreference defaultPref = new UserPreference();
359+
defaultPref.setKey("notifications");
360+
defaultPref.setValue("true");
361+
profile.addPreference(defaultPref);
362+
}
363+
}
364+
profileRepository.saveAll(profiles);
365+
}
366+
}
367+
```
368+
369+
## Troubleshooting
370+
371+
**Profile Not Loading After Authentication**
372+
- Ensure your `AuthenticationListener` is properly registered as a Spring bean
373+
- Verify that Spring Security is configured to use the framework's authentication provider
374+
- Check that your transaction boundaries are correctly defined
375+
376+
**Session Profile Returns Null**
377+
- Make sure the session scoping is correctly configured
378+
- Ensure authentication events are being fired
379+
- Check for circular dependencies in your profile service
380+
381+
**Missing Profile Data**
382+
- Verify that profile initialization logic correctly sets default values
383+
- Check that database schema updates include new fields
384+
- Review transaction isolation levels if concurrent updates are possible
385+
386+
For more complex issues, enable debug logging:
387+
388+
```yaml
389+
logging:
390+
level:
391+
com.digitalsanctuary.spring.user.profile: DEBUG
392+
com.example.myapp.profile: DEBUG
393+
```
394+
395+
---
396+
397+
This framework provides a flexible foundation for managing user-specific data in your application. By extending these base components, you can create a rich user experience while maintaining clean separation of concerns and leveraging Spring's powerful features.
398+
399+
For a complete working example, refer to the [Spring User Framework Demo Application](https://github.com/devondragon/SpringUserFrameworkDemoApp).

0 commit comments

Comments
 (0)