Skip to content
Merged
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
14 changes: 12 additions & 2 deletions src/main/java/com/github/wellch4n/oops/config/JwtAuthFilter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.github.wellch4n.oops.config;

import com.github.wellch4n.oops.data.User;
import com.github.wellch4n.oops.data.UserRepository;
import com.github.wellch4n.oops.objects.AuthUserPrincipal;
import com.github.wellch4n.oops.utils.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand All @@ -18,9 +21,11 @@
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtUtils jwtUtils;
private final UserRepository userRepository;

public JwtAuthFilter(JwtUtils jwtUtils) {
public JwtAuthFilter(JwtUtils jwtUtils, UserRepository userRepository) {
this.jwtUtils = jwtUtils;
this.userRepository = userRepository;
}

@Override
Expand All @@ -40,9 +45,14 @@ protected void doFilterInternal(@NonNull HttpServletRequest request,
}
if (token != null && jwtUtils.isValid(token)) {
String username = jwtUtils.getUsername(token);
String userId = jwtUtils.getUserId(token);
if (userId == null || userId.isBlank()) {
userId = userRepository.findByUsername(username).map(User::getId).orElse(null);
}
String role = jwtUtils.getRole(token);
AuthUserPrincipal principal = new AuthUserPrincipal(userId, username);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username, null, List.of(new SimpleGrantedAuthority("ROLE_" + role))
principal, null, List.of(new SimpleGrantedAuthority("ROLE_" + role))
);
Comment on lines 46 to 56
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userId can still end up null here (e.g., legacy token without userId claim + username not found). The filter still authenticates the request with an AuthUserPrincipal(null, username), which will later cause failures (e.g., /api/users/me calling findById(null) throws) and may allow creating apps with an unverified owner. Consider treating the token as invalid when userId cannot be resolved (skip setting authentication), or explicitly fail the request, rather than authenticating with a null userId.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

SecurityContextHolder.getContext().setAuthentication(auth);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

import com.github.wellch4n.oops.data.*;
import com.github.wellch4n.oops.enums.DeployMode;
import com.github.wellch4n.oops.objects.AuthUserPrincipal;
import com.github.wellch4n.oops.objects.ApplicationPodStatusResponse;
import com.github.wellch4n.oops.objects.ApplicationResponse;
import com.github.wellch4n.oops.objects.ClusterDomainResponse;
import com.github.wellch4n.oops.objects.Page;
import com.github.wellch4n.oops.objects.Result;
import com.github.wellch4n.oops.service.ApplicationService;
import com.github.wellch4n.oops.service.DeploymentService;
import com.github.wellch4n.oops.service.PipelineService;
import java.util.List;

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

/**
Expand All @@ -32,22 +36,24 @@ public ApplicationController(ApplicationService applicationService, DeploymentSe
}

@GetMapping("/{name}")
public Result<Application> getApplication(@PathVariable String namespace, @PathVariable String name) {
return Result.success(applicationService.getApplication(namespace, name));
public Result<ApplicationResponse> getApplication(@PathVariable String namespace, @PathVariable String name) {
return Result.success(applicationService.getApplicationResponse(namespace, name));
}

@GetMapping
public Result<Page<Application>> getApplications(@PathVariable String namespace,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
public Result<Page<ApplicationResponse>> getApplications(@PathVariable String namespace,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return Result.success(applicationService.getApplications(namespace, keyword, page, size));
}

@PostMapping
public Result<String> createApplication(@PathVariable String namespace,
@RequestBody Application application) {
return Result.success(applicationService.createApplication(namespace, application));
@RequestBody Application application,
Authentication authentication) {
AuthUserPrincipal principal = (AuthUserPrincipal) authentication.getPrincipal();
return Result.success(applicationService.createApplication(namespace, application, principal.userId()));
}

@PutMapping("/{name}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public Result<LoginResponse> login(@RequestBody LoginRequest request) {
return Result.failure("用户名或密码错误");
}
User user = userOpt.get();
String token = jwtUtils.generateToken(user.getUsername(), user.getRole().name());
return Result.success(new LoginResponse(token, user.getUsername(), user.getRole()));
String token = jwtUtils.generateToken(user.getId(), user.getUsername(), user.getRole().name());
return Result.success(new LoginResponse(token, user.getId(), user.getUsername(), user.getRole()));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.github.wellch4n.oops.controller;

import com.github.wellch4n.oops.data.Application;
import com.github.wellch4n.oops.objects.ApplicationResponse;
import com.github.wellch4n.oops.objects.Result;
import com.github.wellch4n.oops.service.ApplicationService;
import java.util.List;
Expand All @@ -25,7 +25,7 @@ public SearchController(ApplicationService applicationService) {
}

@GetMapping("/applications")
public Result<List<Application>> searchApplications(@RequestParam(required = false) String keyword, @RequestParam(defaultValue = "5") int size) {
public Result<List<ApplicationResponse>> searchApplications(@RequestParam(required = false) String keyword, @RequestParam(defaultValue = "5") int size) {
return Result.success(applicationService.searchApplications(keyword, size));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.github.wellch4n.oops.data.User;
import com.github.wellch4n.oops.enums.UserRole;
import com.github.wellch4n.oops.objects.AuthUserPrincipal;
import com.github.wellch4n.oops.objects.CreateUserRequest;
import com.github.wellch4n.oops.objects.Result;
import com.github.wellch4n.oops.objects.UpdateUserRequest;
Expand Down Expand Up @@ -36,7 +37,8 @@ public Result<List<User>> listUsers() {
@GetMapping("/me")
@PreAuthorize("isAuthenticated()")
public Result<User> me(org.springframework.security.core.Authentication authentication) {
return userService.findByUsername(authentication.getName())
AuthUserPrincipal principal = (AuthUserPrincipal) authentication.getPrincipal();
return userService.findById(principal.userId())
.map(Result::success)
.orElse(Result.failure("用户不存在"));
Comment on lines 39 to 43
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

principal.userId() may be null (e.g., legacy JWT missing userId where username→id lookup fails). Passing null into userService.findById will throw in Spring Data, resulting in a 500 for /me. Please guard against a missing userId (e.g., fall back to authentication.getName()/principal.username() lookup, or return an auth failure) to keep backward compatibility safe.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/github/wellch4n/oops/data/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public class Application extends BaseDataObject {
private String description;

private String namespace;

private String owner;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.github.wellch4n.oops.objects;

import com.github.wellch4n.oops.data.Application;
import java.time.LocalDateTime;

public record ApplicationResponse(
String id,
LocalDateTime createdTime,
String name,
String description,
String namespace,
String owner,
String ownerName
) {
public static ApplicationResponse from(Application application, String ownerName) {
return new ApplicationResponse(
application.getId(),
application.getCreatedTime(),
application.getName(),
application.getDescription(),
application.getNamespace(),
application.getOwner(),
ownerName
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.github.wellch4n.oops.objects;

import java.security.Principal;

public record AuthUserPrincipal(String userId, String username) implements Principal {

@Override
public String getName() {
return username;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

import com.github.wellch4n.oops.enums.UserRole;

public record LoginResponse(String token, String username, UserRole role) {}
public record LoginResponse(String token, String id, String username, UserRole role) {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
import com.github.wellch4n.oops.data.*;
import com.github.wellch4n.oops.enums.OopsTypes;
import com.github.wellch4n.oops.objects.ApplicationPodStatusResponse;
import com.github.wellch4n.oops.objects.ApplicationResponse;
import com.github.wellch4n.oops.objects.ClusterDomainResponse;
import com.github.wellch4n.oops.objects.Page;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.Quantity;
import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.PageRequest;
Expand All @@ -36,39 +39,54 @@ public class ApplicationService {
private final ApplicationEnvironmentRepository applicationEnvironmentRepository;
private final ApplicationServiceConfigRepository applicationServiceConfigRepository;
private final EnvironmentRepository environmentRepository;
private final UserService userService;

public ApplicationService(ApplicationRepository applicationRepository,
ApplicationBuildConfigRepository applicationBuildConfigRepository,
ApplicationPerformanceConfigRepository applicationPerformanceConfigRepository,
ApplicationEnvironmentRepository applicationEnvironmentRepository, ApplicationServiceConfigRepository applicationServiceConfigRepository,
EnvironmentRepository environmentRepository) {
EnvironmentRepository environmentRepository,
UserService userService) {
this.applicationRepository = applicationRepository;
this.applicationBuildConfigRepository = applicationBuildConfigRepository;
this.applicationPerformanceConfigRepository = applicationPerformanceConfigRepository;
this.applicationEnvironmentRepository = applicationEnvironmentRepository;
this.applicationServiceConfigRepository = applicationServiceConfigRepository;
this.environmentRepository = environmentRepository;
this.userService = userService;
}

public Application getApplication(String namespace, String name) {
return applicationRepository.findByNamespaceAndName(namespace, name);
}

public Page<Application> getApplications(String namespace, String keyword, int page, int size) {
public ApplicationResponse getApplicationResponse(String namespace, String name) {
return toApplicationResponse(applicationRepository.findByNamespaceAndName(namespace, name));
}

public Page<ApplicationResponse> getApplications(String namespace, String keyword, int page, int size) {
Pageable pageable = PageRequest.of(Math.max(page - 1, 0), size);
return Page.of(applicationRepository.findByNamespaceAndNameContainingIgnoreCase(
namespace, StringUtils.defaultIfBlank(keyword, ""), pageable));
org.springframework.data.domain.Page<Application> applicationPage =
applicationRepository.findByNamespaceAndNameContainingIgnoreCase(
namespace, StringUtils.defaultIfBlank(keyword, ""), pageable);
return new Page<>(
applicationPage.getTotalElements(),
toApplicationResponses(applicationPage.getContent()),
applicationPage.getSize(),
applicationPage.getTotalPages()
);
}

public List<Application> searchApplications(String keyword, int size) {
public List<ApplicationResponse> searchApplications(String keyword, int size) {
List<Application> applications = applicationRepository.findByNameContainingIgnoreCase(
StringUtils.defaultIfBlank(keyword, ""));
return applications.stream().limit(size).toList();
return toApplicationResponses(applications.stream().limit(size).toList());
}

@Transactional
public String createApplication(String namespace, Application application) {
public String createApplication(String namespace, Application application, String creatorUserId) {
application.setNamespace(namespace);
application.setOwner(normalizeOwner(creatorUserId));
applicationRepository.save(application);
return application.getId();
}
Expand All @@ -79,11 +97,47 @@ public Boolean updateApplication(String namespace, String name, Application appl
if (exist == null) {
throw new RuntimeException("Application not found");
}
application.setDescription(application.getDescription());
applicationRepository.save(application);
exist.setDescription(application.getDescription());
exist.setOwner(normalizeOwner(application.getOwner()));
applicationRepository.save(exist);
return true;
}

private String normalizeOwner(String owner) {
if (StringUtils.isBlank(owner)) {
return null;
}
Optional<User> user = userService.findById(owner);
if (user.isEmpty()) {
throw new RuntimeException("Owner user not found");
}
return owner;
}

private List<ApplicationResponse> toApplicationResponses(List<Application> applications) {
Set<String> ownerIds = applications.stream()
.map(Application::getOwner)
.filter(StringUtils::isNotBlank)
.collect(java.util.stream.Collectors.toSet());
Map<String, String> ownerNameMap = userService.getUsernameMapByIds(ownerIds);
return applications.stream()
.map(application -> ApplicationResponse.from(application, ownerNameMap.get(application.getOwner())))
.toList();
}

private ApplicationResponse toApplicationResponse(Application application) {
if (application == null) {
return null;
}
String ownerName = null;
if (StringUtils.isNotBlank(application.getOwner())) {
ownerName = userService.findById(application.getOwner())
.map(User::getUsername)
.orElse(null);
}
return ApplicationResponse.from(application, ownerName);
}

@Transactional
public Boolean updateApplicationBuildConfig(String namespace, String name, ApplicationBuildConfig request) {
ApplicationBuildConfig buildConfig = applicationBuildConfigRepository.findByNamespaceAndApplicationName(namespace, name)
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/github/wellch4n/oops/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import com.github.wellch4n.oops.data.User;
import com.github.wellch4n.oops.data.UserRepository;
import com.github.wellch4n.oops.enums.UserRole;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

Expand All @@ -31,6 +34,14 @@ public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email);
}

public Map<String, String> getUsernameMapByIds(Collection<String> ids) {
if (ids == null || ids.isEmpty()) {
return Map.of();
}
return userRepository.findAllById(ids).stream()
.collect(Collectors.toMap(User::getId, User::getUsername, (left, right) -> left));
}

public Optional<User> findByUsernameOrEmail(String identifier) {
Optional<User> user = userRepository.findByUsername(identifier);
if (user.isPresent()) return user;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public String authenticate(String code) throws IOException {
externalAccountRepository.save(account);
}

return jwtUtils.generateToken(user.getUsername(), user.getRole().name());
return jwtUtils.generateToken(user.getId(), user.getUsername(), user.getRole().name());
}

private User findOrCreateUser(String name, String email) {
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/com/github/wellch4n/oops/utils/JwtUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}

public String generateToken(String username, String role) {
public String generateToken(String userId, String username, String role) {
return Jwts.builder()
.subject(username)
.claim("userId", userId)
.claim("role", role)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
Expand All @@ -44,6 +45,10 @@ public String getUsername(String token) {
return parseToken(token).getSubject();
}

public String getUserId(String token) {
return parseToken(token).get("userId", String.class);
}

public String getRole(String token) {
return parseToken(token).get("role", String.class);
}
Expand Down
10 changes: 10 additions & 0 deletions web/app/apps/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ export const getColumns = (t: (key: string) => string): ColumnDef<Application>[]
accessorKey: "namespace",
header: t("apps.col.namespace"),
},
{
accessorKey: "owner",
header: t("apps.col.owner"),
cell: ({ row }) => {
if (!row.original.owner) {
return t("common.unassigned")
}
return row.original.ownerName || row.original.owner
},
},
{
id: "actions",
cell: ({ row, table }) => {
Expand Down
Loading
Loading