feat: Add application owners#13
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class “application owner” support end-to-end (DB/API/UI) and extends authentication/JWT plumbing to propagate userId alongside username so ownership can be set automatically and displayed consistently.
Changes:
- Add
ownerto applications and exposeownerNamevia a newApplicationResponsereturned by app detail/list/search APIs. - Extend auth/JWT to include
userId, expose it in login responses and frontend auth state, and provide backward-compatible JWT parsing. - Update the web UI to view/edit owner (basic info form, apps list, command palette, recent apps) with new i18n strings.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| web/store/recent-app.ts | Persists ownerName for recent app entries. |
| web/locales/zh.ts | Adds owner-related i18n strings (ZH). |
| web/locales/en.ts | Adds owner-related i18n strings (EN). |
| web/lib/auth.ts | Stores auth_user_id and exposes getUserId(). |
| web/lib/api/types.ts | Extends Application type with owner/ownerName; relaxes workspaceId. |
| web/lib/api/auth.ts | Includes id in login result and passes it into setAuth. |
| web/components/command-palette.tsx | Displays owner in palette results and persists it in recent app state. |
| web/app/auth/feishu/callback/page.tsx | Updates Feishu callback flow to store userId in auth state. |
| web/app/apps/schema.ts | Allows owner field in the application basic schema. |
| web/app/apps/components/application-create-dialog.tsx | Refactors reset logic when dialog opens. |
| web/app/apps/components/application-basic-info.tsx | Adds owner selector populated from users API; wires owner into form defaults. |
| web/app/apps/columns.tsx | Adds “Owner” column to applications table. |
| src/main/java/com/github/wellch4n/oops/utils/JwtUtils.java | Adds userId claim to JWT and helper to read it. |
| src/main/java/com/github/wellch4n/oops/service/UserService.java | Adds helper to map user IDs to usernames in bulk. |
| src/main/java/com/github/wellch4n/oops/service/external/FeishuAuthStrategy.java | Generates JWTs with userId claim. |
| src/main/java/com/github/wellch4n/oops/service/ApplicationService.java | Sets owner on create; normalizes/validates owner; returns ApplicationResponse with ownerName for list/search/detail. |
| src/main/java/com/github/wellch4n/oops/objects/LoginResponse.java | Extends login response to include id. |
| src/main/java/com/github/wellch4n/oops/objects/AuthUserPrincipal.java | Introduces principal carrying userId + username. |
| src/main/java/com/github/wellch4n/oops/objects/ApplicationResponse.java | New response DTO including owner + ownerName. |
| src/main/java/com/github/wellch4n/oops/data/Application.java | Adds owner field to application entity. |
| src/main/java/com/github/wellch4n/oops/controller/UserController.java | Updates /me to resolve current user by userId. |
| src/main/java/com/github/wellch4n/oops/controller/SearchController.java | Returns ApplicationResponse from app search endpoint. |
| src/main/java/com/github/wellch4n/oops/controller/AuthController.java | Returns id in login response and embeds userId in JWT. |
| src/main/java/com/github/wellch4n/oops/controller/ApplicationController.java | Returns ApplicationResponse for app detail/list; sets owner from authenticated principal on create. |
| src/main/java/com/github/wellch4n/oops/config/JwtAuthFilter.java | Builds AuthUserPrincipal from JWT userId (fallback to username lookup for legacy tokens). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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)) | ||
| ); |
There was a problem hiding this comment.
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.
| 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("用户不存在")); |
There was a problem hiding this comment.
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.
| const envRes = await fetchEnvironments() | ||
| setEnvironments(envRes.data) | ||
|
|
||
| setUsers(await fetchUsers()) | ||
|
|
There was a problem hiding this comment.
This new owner selector loads the full /api/users list into the app edit form. Since the backend currently returns User entities directly (which include sensitive fields like email and password hash), this makes it easy for any user who can access this screen to retrieve sensitive user data via the network response. Consider switching this call to a dedicated “user lookup for owner selection” endpoint/DTO that only returns id + username (and enforce appropriate authorization on /api/users).
Summary
ownerto applications and set it automatically from the authenticated user when creating an applicationuserIdalongsideusername, and expose the user id in login responses and frontend auth stateApplicationResponsewithownerandownerName, and return it from application detail, list, and search endpointsuserIdfromusernamein the auth filterTesting
./mvnw -q -DskipTests compilepnpm exec eslint lib/api/auth.ts lib/auth.ts app/auth/feishu/callback/page.tsxpnpm exec eslint app/apps/page.tsx app/apps/columns.tsx lib/api/types.ts components/command-palette.tsx 'app/apps/[namespace]/[name]/page.tsx' app/apps/components/application-basic-info.tsx lib/api/applications.tspnpm exec eslint components/command-palette.tsx store/recent-app.ts./mvnw -q spring-boot:run