Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
package com.crystalpdf.backend.repository;

package com.crystalpdf.backend.repository;
import com.crystalpdf.backend.entity.Document;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Collection;
import java.util.List;
import java.util.Optional;

public interface DocumentRepository extends JpaRepository<Document, Long> {
List<Document> findByOwnerIdOrderByCreatedAtDesc(Long userId);
Optional<Document> findByIdAndOwnerId(Long id, Long userId);

@Query("""
select d.owner.id as ownerId,
count(d) as fileCount,
coalesce(sum(d.sizeBytes), 0) as totalSizeBytes
from Document d
where d.owner.id in :ownerIds
group by d.owner.id
""")
List<OwnerStorageStats> findStorageStatsByOwnerIds(@Param("ownerIds") Collection<Long> ownerIds);

interface OwnerStorageStats {
Long getOwnerId();
Long getFileCount();
Long getTotalSizeBytes();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.crystalpdf.backend.repository;

import com.crystalpdf.backend.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

Expand All @@ -10,4 +14,16 @@ public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByEmail(String email);
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
long countByAdminTrue();

@Query("""
select u from User u
where (:search is null
or lower(u.email) like :search
or lower(coalesce(u.username, '')) like :search)
and (:adminOnly is null or u.admin = :adminOnly)
""")
Page<User> findForAdminList(@Param("search") String search,
@Param("adminOnly") Boolean adminOnly,
Pageable pageable);
}
287 changes: 139 additions & 148 deletions backend/src/main/java/com/crystalpdf/backend/service/AdminService.java
Original file line number Diff line number Diff line change
@@ -1,180 +1,171 @@
package com.crystalpdf.backend.service;

import com.crystalpdf.backend.dto.AdminUserResponse;
import com.crystalpdf.backend.dto.AppSettingsResponse;
import com.crystalpdf.backend.dto.UpdateAppSettingsRequest;
import com.crystalpdf.backend.entity.AppSettings;
import com.crystalpdf.backend.entity.Document;
import com.crystalpdf.backend.entity.User;
package com.crystalpdf.backend.service;
import com.crystalpdf.backend.dto.AdminUserResponse;
import com.crystalpdf.backend.dto.AppSettingsResponse;
import com.crystalpdf.backend.dto.UpdateAppSettingsRequest;
import com.crystalpdf.backend.entity.AppSettings;
import com.crystalpdf.backend.entity.Document;
import com.crystalpdf.backend.entity.User;
import com.crystalpdf.backend.repository.AppSettingsRepository;
import com.crystalpdf.backend.repository.DocumentRepository;
import com.crystalpdf.backend.repository.UserRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;

import java.io.File;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Service
public class AdminService {

private final UserRepository userRepository;
private final DocumentRepository documentRepository;
private final StorageService storageService;
private final AppSettingsRepository settingsRepository;
private final PasswordEncoder passwordEncoder;

@Value("${crystalpdf.storage.path}")
private String storagePath;

public AdminService(UserRepository userRepository, DocumentRepository documentRepository,
StorageService storageService, AppSettingsRepository settingsRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.documentRepository = documentRepository;
this.storageService = storageService;
this.settingsRepository = settingsRepository;
this.passwordEncoder = passwordEncoder;
import java.util.Map;
@Service
public class AdminService {
private final UserRepository userRepository;
private final DocumentRepository documentRepository;
private final StorageService storageService;
private final AppSettingsRepository settingsRepository;
private final PasswordEncoder passwordEncoder;
@Value("${crystalpdf.storage.path}")
private String storagePath;
public AdminService(UserRepository userRepository, DocumentRepository documentRepository,
StorageService storageService, AppSettingsRepository settingsRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.documentRepository = documentRepository;
this.storageService = storageService;
this.settingsRepository = settingsRepository;
this.passwordEncoder = passwordEncoder;
}

public Page<AdminUserResponse> getAllUsers(int page, int pageSize, String search, String filter) {
List<User> allUsers = userRepository.findAll();

// Apply search filter
if (search != null && !search.isBlank()) {
String q = search.toLowerCase();
allUsers = allUsers.stream()
.filter(u -> u.getEmail().toLowerCase().contains(q) ||
u.getDisplayUsername().toLowerCase().contains(q))
.toList();
String searchPattern = search != null && !search.isBlank()
? "%" + search.toLowerCase() + "%"
: null;
Boolean adminOnly = null;
if ("admin".equalsIgnoreCase(filter)) {
adminOnly = true;
} else if ("regular".equalsIgnoreCase(filter)) {
adminOnly = false;
}

// Apply admin filter
if (filter != null && !filter.isBlank()) {
if ("admin".equalsIgnoreCase(filter)) {
allUsers = allUsers.stream().filter(User::isAdmin).toList();
} else if ("regular".equalsIgnoreCase(filter)) {
allUsers = allUsers.stream().filter(u -> !u.isAdmin()).toList();
}
}

// Convert to responses
List<AdminUserResponse> responses = allUsers.stream().map(user -> {
List<Document> docs = documentRepository.findByOwnerIdOrderByCreatedAtDesc(user.getId());
long storageUsed = docs.stream().mapToLong(Document::getSizeBytes).sum();
AppSettings settings = getSettingsEntity();
Page<User> users = userRepository.findForAdminList(searchPattern, adminOnly, PageRequest.of(page, pageSize));
List<Long> userIds = users.getContent().stream().map(User::getId).toList();
Map<Long, DocumentRepository.OwnerStorageStats> storageStats = userIds.isEmpty()
? Map.of()
: documentRepository.findStorageStatsByOwnerIds(userIds).stream()
.collect(java.util.stream.Collectors.toMap(
DocumentRepository.OwnerStorageStats::getOwnerId,
stats -> stats
));
AppSettings settings = getSettingsEntity();

return users.map(user -> {
DocumentRepository.OwnerStorageStats stats = storageStats.get(user.getId());
long storageUsed = stats != null && stats.getTotalSizeBytes() != null ? stats.getTotalSizeBytes() : 0L;
int fileCount = stats != null && stats.getFileCount() != null ? Math.toIntExact(stats.getFileCount()) : 0;
long limitBytes = user.getStorageLimitBytes() != null ? user.getStorageLimitBytes()
: settings.getDefaultStorageLimitMb() * 1024L * 1024L;
return new AdminUserResponse(
user.getId(), user.getEmail(), user.getDisplayUsername(),
user.isAdmin(), user.isPasswordChangeRequired(),
limitBytes, storageUsed, docs.size(),
limitBytes, storageUsed, fileCount,
user.getCreatedAt() != null ? user.getCreatedAt().toString() : ""
);
}).toList();

// Apply pagination
int start = page * pageSize;
int end = Math.min(start + pageSize, responses.size());
List<AdminUserResponse> pageContent = responses.subList(start, end);
int totalPages = (int) Math.ceil((double) responses.size() / pageSize);

return new PageImpl<>(pageContent, PageRequest.of(page, pageSize), responses.size());
}

public void setStorageLimit(Long userId, Long limitBytes) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found."));
user.setStorageLimitBytes(limitBytes);
userRepository.save(user);
}

@Transactional
public void deleteUser(Long userId, User requestingAdmin) {
if (userId.equals(requestingAdmin.getId())) {
throw new IllegalArgumentException("Cannot delete your own admin account.");
}
User target = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found."));
List<Document> docs = documentRepository.findByOwnerIdOrderByCreatedAtDesc(target.getId());
for (Document doc : docs) storageService.deleteFile(doc);
documentRepository.deleteAll(docs);
userRepository.delete(target);
});
}

public AppSettingsResponse getSettings() {
AppSettings s = getSettingsEntity();
return new AppSettingsResponse(s.isAllowRegistration(), s.getMaxUploadSizeMb(), s.getDefaultStorageLimitMb(), s.isMaintenanceMode());
}

public AppSettingsResponse updateSettings(UpdateAppSettingsRequest req) {
AppSettings s = getSettingsEntity();
if (req.allowRegistration() != null) s.setAllowRegistration(req.allowRegistration());
if (req.maxUploadSizeMb() != null) s.setMaxUploadSizeMb(req.maxUploadSizeMb());
if (req.defaultStorageLimitMb() != null) s.setDefaultStorageLimitMb(req.defaultStorageLimitMb());
if (req.maintenanceMode() != null) s.setMaintenanceMode(req.maintenanceMode());
settingsRepository.save(s);
return new AppSettingsResponse(s.isAllowRegistration(), s.getMaxUploadSizeMb(), s.getDefaultStorageLimitMb(), s.isMaintenanceMode());
}

/** Toggle admin status of a user. Cannot demote self. */
public void toggleAdmin(Long userId, User requestingAdmin) {
if (userId.equals(requestingAdmin.getId())) {
throw new IllegalArgumentException("Cannot change your own admin status.");
}
User target = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found."));
target.setAdmin(!target.isAdmin());
userRepository.save(target);
}

/** Force a user to change their password on next login. Returns the plaintext temp password. */
public String forcePasswordReset(Long userId) {
User target = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found."));
String tempPassword = java.util.UUID.randomUUID().toString().substring(0, 12);
target.setPassword(passwordEncoder.encode(tempPassword));
target.setPasswordChangeRequired(true);
userRepository.save(target);
return tempPassword;
}

/** Returns platform system information. */
public Map<String, Object> getSystemInfo() {
Map<String, Object> info = new LinkedHashMap<>();

// JVM
Runtime rt = Runtime.getRuntime();
info.put("jvmMaxMemoryMb", rt.maxMemory() / (1024 * 1024));
info.put("jvmUsedMemoryMb", (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024));
info.put("jvmTotalMemoryMb", rt.totalMemory() / (1024 * 1024));
info.put("availableProcessors", rt.availableProcessors());

// Disk
File storage = new File(storagePath);
info.put("diskTotalMb", storage.getTotalSpace() / (1024 * 1024));
info.put("diskFreeMb", storage.getFreeSpace() / (1024 * 1024));
info.put("diskUsableMb", storage.getUsableSpace() / (1024 * 1024));


public void setStorageLimit(Long userId, Long limitBytes) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found."));
user.setStorageLimitBytes(limitBytes);
userRepository.save(user);
}

@Transactional
public void deleteUser(Long userId, User requestingAdmin) {
if (userId.equals(requestingAdmin.getId())) {
throw new IllegalArgumentException("Cannot delete your own admin account.");
}
User target = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found."));
List<Document> docs = documentRepository.findByOwnerIdOrderByCreatedAtDesc(target.getId());
for (Document doc : docs) storageService.deleteFile(doc);
documentRepository.deleteAll(docs);
userRepository.delete(target);
}

public AppSettingsResponse getSettings() {
AppSettings s = getSettingsEntity();
return new AppSettingsResponse(s.isAllowRegistration(), s.getMaxUploadSizeMb(), s.getDefaultStorageLimitMb(), s.isMaintenanceMode());
}

public AppSettingsResponse updateSettings(UpdateAppSettingsRequest req) {
AppSettings s = getSettingsEntity();
if (req.allowRegistration() != null) s.setAllowRegistration(req.allowRegistration());
if (req.maxUploadSizeMb() != null) s.setMaxUploadSizeMb(req.maxUploadSizeMb());
if (req.defaultStorageLimitMb() != null) s.setDefaultStorageLimitMb(req.defaultStorageLimitMb());
if (req.maintenanceMode() != null) s.setMaintenanceMode(req.maintenanceMode());
settingsRepository.save(s);
return new AppSettingsResponse(s.isAllowRegistration(), s.getMaxUploadSizeMb(), s.getDefaultStorageLimitMb(), s.isMaintenanceMode());
}

/** Toggle admin status of a user. Cannot demote self. */
public void toggleAdmin(Long userId, User requestingAdmin) {
if (userId.equals(requestingAdmin.getId())) {
throw new IllegalArgumentException("Cannot change your own admin status.");
}
User target = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found."));
target.setAdmin(!target.isAdmin());
userRepository.save(target);
}

/** Force a user to change their password on next login. Returns the plaintext temp password. */
public String forcePasswordReset(Long userId) {
User target = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found."));
String tempPassword = java.util.UUID.randomUUID().toString().substring(0, 12);
target.setPassword(passwordEncoder.encode(tempPassword));
target.setPasswordChangeRequired(true);
userRepository.save(target);
return tempPassword;
}

/** Returns platform system information. */
public Map<String, Object> getSystemInfo() {
Map<String, Object> info = new LinkedHashMap<>();

// JVM
Runtime rt = Runtime.getRuntime();
info.put("jvmMaxMemoryMb", rt.maxMemory() / (1024 * 1024));
info.put("jvmUsedMemoryMb", (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024));
info.put("jvmTotalMemoryMb", rt.totalMemory() / (1024 * 1024));
info.put("availableProcessors", rt.availableProcessors());

// Disk
File storage = new File(storagePath);
info.put("diskTotalMb", storage.getTotalSpace() / (1024 * 1024));
info.put("diskFreeMb", storage.getFreeSpace() / (1024 * 1024));
info.put("diskUsableMb", storage.getUsableSpace() / (1024 * 1024));

// Platform stats
info.put("totalUsers", userRepository.count());
info.put("totalFiles", documentRepository.count());
info.put("totalAdmins", userRepository.findAll().stream().filter(User::isAdmin).count());
info.put("totalAdmins", userRepository.countByAdminTrue());
info.put("javaVersion", System.getProperty("java.version"));
info.put("osName", System.getProperty("os.name"));

return info;
}

private AppSettings getSettingsEntity() {
return settingsRepository.findById(1L).orElseGet(() -> settingsRepository.save(new AppSettings()));
}
}
return info;
}
private AppSettings getSettingsEntity() {
return settingsRepository.findById(1L).orElseGet(() -> settingsRepository.save(new AppSettings()));
}
}
Loading