user sync optinization
This commit is contained in:
@@ -8,9 +8,11 @@ describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
const mockCreateFromToken = jest.fn();
|
||||
const mockFindByKeycloakSub = jest.fn();
|
||||
|
||||
const mockUsersService = {
|
||||
createFromToken: mockCreateFromToken,
|
||||
findByKeycloakSub: mockFindByKeycloakSub,
|
||||
};
|
||||
|
||||
const mockAuthUser: AuthenticatedUser = {
|
||||
@@ -202,6 +204,81 @@ describe('AuthService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureUserExists', () => {
|
||||
it('should return existing user without updating', async () => {
|
||||
mockFindByKeycloakSub.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.ensureUserExists(mockAuthUser);
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
expect(mockFindByKeycloakSub).toHaveBeenCalledWith('f:realm:user123');
|
||||
expect(mockCreateFromToken).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create new user if user does not exist', async () => {
|
||||
mockFindByKeycloakSub.mockResolvedValue(null);
|
||||
mockCreateFromToken.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.ensureUserExists(mockAuthUser);
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
expect(mockFindByKeycloakSub).toHaveBeenCalledWith('f:realm:user123');
|
||||
expect(mockCreateFromToken).toHaveBeenCalledWith({
|
||||
keycloakSub: 'f:realm:user123',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
username: 'testuser',
|
||||
picture: 'https://example.com/avatar.jpg',
|
||||
roles: ['user', 'premium'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle user with no email when creating new user', async () => {
|
||||
const authUserNoEmail: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:user456',
|
||||
name: 'No Email User',
|
||||
};
|
||||
|
||||
mockFindByKeycloakSub.mockResolvedValue(null);
|
||||
mockCreateFromToken.mockResolvedValue({
|
||||
...mockUser,
|
||||
email: '',
|
||||
name: 'No Email User',
|
||||
});
|
||||
|
||||
await service.ensureUserExists(authUserNoEmail);
|
||||
|
||||
expect(mockFindByKeycloakSub).toHaveBeenCalledWith('f:realm:user456');
|
||||
expect(mockCreateFromToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
email: '',
|
||||
name: 'No Email User',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use "Unknown User" when creating user with no name or username', async () => {
|
||||
const authUserMinimal: AuthenticatedUser = {
|
||||
keycloakSub: 'f:realm:minimal',
|
||||
};
|
||||
|
||||
mockFindByKeycloakSub.mockResolvedValue(null);
|
||||
mockCreateFromToken.mockResolvedValue({
|
||||
...mockUser,
|
||||
name: 'Unknown User',
|
||||
});
|
||||
|
||||
await service.ensureUserExists(authUserMinimal);
|
||||
|
||||
expect(mockFindByKeycloakSub).toHaveBeenCalledWith('f:realm:minimal');
|
||||
expect(mockCreateFromToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'Unknown User',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasRole', () => {
|
||||
it('should return true if user has the required role', () => {
|
||||
const result = service.hasRole(mockAuthUser, 'user');
|
||||
|
||||
@@ -24,6 +24,11 @@ import { User } from '../users/users.entity';
|
||||
*
|
||||
* For explicit profile sync (webhooks, admin operations), use:
|
||||
* - UsersService.syncProfileFromToken() - force sync regardless of changes
|
||||
*
|
||||
* ## Usage Guidelines
|
||||
*
|
||||
* - Use `syncUserFromToken()` for actual login events (WebSocket connections, /users/me)
|
||||
* - Use `ensureUserExists()` for regular API calls that just need the user record
|
||||
*/
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -61,6 +66,49 @@ export class AuthService {
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a user exists in the local database without updating profile data.
|
||||
* This is optimized for regular API calls that need the user record but don't
|
||||
* need to sync profile data from Keycloak on every request.
|
||||
*
|
||||
* - For new users: Creates with full profile data
|
||||
* - For existing users: Returns existing record WITHOUT updating
|
||||
*
|
||||
* Use this for most API endpoints. Only use syncUserFromToken() for actual
|
||||
* login events (WebSocket connections, /users/me endpoint).
|
||||
*
|
||||
* @param authenticatedUser - User data extracted from JWT token
|
||||
* @returns The user entity
|
||||
*/
|
||||
async ensureUserExists(authenticatedUser: AuthenticatedUser): Promise<User> {
|
||||
const { keycloakSub, email, name, username, picture, roles } =
|
||||
authenticatedUser;
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = await this.usersService.findByKeycloakSub(keycloakSub);
|
||||
|
||||
if (existingUser) {
|
||||
// User exists - return without updating
|
||||
return existingUser;
|
||||
}
|
||||
|
||||
// New user - create with full profile data
|
||||
this.logger.log(
|
||||
`Creating new user from token: ${keycloakSub} (via ensureUserExists)`,
|
||||
);
|
||||
|
||||
const user = await this.usersService.createFromToken({
|
||||
keycloakSub,
|
||||
email: email || '',
|
||||
name: name || username || 'Unknown User',
|
||||
username,
|
||||
picture,
|
||||
roles,
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a user has a specific role.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user