feat: hide apps without launch url on My Apps page

This commit is contained in:
Elias Schneider
2026-08-10 22:53:35 +02:00
parent 84a58cd757
commit 3ca9a55c71
21 changed files with 547 additions and 108 deletions
@@ -394,6 +394,7 @@ func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) error {
// @Param pagination[limit] query int false "Number of items per page" default(20)
// @Param sort[column] query string false "Column to sort by"
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Param filters[hasLaunchURL] query bool false "Filter clients by whether a launch URL is configured"
// @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto]
// @Router /api/oidc/users/me/authorized-clients [get]
func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) error {
@@ -410,6 +411,7 @@ func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) error
// @Param pagination[limit] query int false "Number of items per page" default(20)
// @Param sort[column] query string false "Column to sort by"
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Param filters[hasLaunchURL] query bool false "Filter clients by whether a launch URL is configured"
// @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto]
// @Router /api/oidc/users/{id}/authorized-clients [get]
func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) error {
@@ -467,6 +469,7 @@ func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) er
// @Param pagination[limit] query int false "Number of items per page" default(20)
// @Param sort[column] query string false "Column to sort by"
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Param filters[hasLaunchURL] query bool false "Filter clients by whether a launch URL is configured"
// @Success 200 {object} dto.Paginated[dto.AccessibleOidcClientDto]
// @Router /api/oidc/users/me/clients [get]
func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) error {
@@ -192,6 +192,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
ID: "606c7782-f2b1-49e5-8ea9-26eb1b06d018",
},
Name: "Immich",
LaunchURL: new("https://immich.local"),
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
CallbackURLs: datatype.StringList{"http://immich.localhost/auth/callback"},
CreatedByID: new(users[1].ID),
+29
View File
@@ -592,6 +592,16 @@ func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string,
Preload("Client").
Where("user_id = ?", userID)
// Apply the launch URL filter before pagination so hidden authorizations have their own page count
if hasLaunchURL, ok := getHasLaunchURLFilter(listRequestOptions); ok {
query = query.Joins("JOIN oidc_clients ON oidc_clients.id = user_authorized_oidc_clients.client_id")
if hasLaunchURL {
query = query.Where("oidc_clients.launch_url IS NOT NULL AND oidc_clients.launch_url <> ''")
} else {
query = query.Where("oidc_clients.launch_url IS NULL OR oidc_clients.launch_url = ''")
}
}
var authorizedClients []model.UserAuthorizedOidcClient
response, err := utils.PaginateFilterAndSort(listRequestOptions, query, &authorizedClients)
@@ -667,6 +677,15 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
WHERE oidc_clients_allowed_user_groups.oidc_client_id = oidc_clients.id
AND oidc_clients_allowed_user_groups.user_group_id IN (?))`, false, userGroupIDs)
// Apply the launch URL filter before pagination so the app launcher never contains empty pages
if hasLaunchURL, ok := getHasLaunchURLFilter(listRequestOptions); ok {
if hasLaunchURL {
query = query.Where("oidc_clients.launch_url IS NOT NULL AND oidc_clients.launch_url <> ''")
} else {
query = query.Where("oidc_clients.launch_url IS NULL OR oidc_clients.launch_url = ''")
}
}
var clients []model.OidcClient
// Handle custom sorting for lastUsedAt column
@@ -705,6 +724,16 @@ func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID stri
return dtos, response, err
}
func getHasLaunchURLFilter(listRequestOptions utils.ListRequestOptions) (bool, bool) {
values := listRequestOptions.Filters["hasLaunchURL"]
if len(values) == 0 {
return false, false
}
hasLaunchURL, ok := values[0].(bool)
return hasLaunchURL, ok
}
func (s *OidcService) GetClientPreview(ctx context.Context, clientID string, userID string, scopes []string, authenticationMethod string) (*dto.OidcClientPreviewDto, error) {
client, err := s.getClientInternal(ctx, clientID, s.db, false)
if err != nil {
@@ -858,6 +858,60 @@ func TestOidcService_ListAccessibleOidcClients_requiresExplicitGroupPermission(t
assert.Equal(t, []string{"Unrestricted"}, accessibleClientNames(noGroupClients))
}
func TestOidcService_ListClientViewsFilterByLaunchURLPresence(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil)
require.NoError(t, err)
user := model.User{Username: "launch-url-filter"}
require.NoError(t, db.Create(&user).Error)
launchURL := "https://launchable.example.com"
emptyLaunchURL := ""
clients := []model.OidcClient{
{Name: "Launchable", LaunchURL: &launchURL},
{Name: "Missing launch URL"},
{Name: "Empty launch URL", LaunchURL: &emptyLaunchURL},
}
for i := range clients {
require.NoError(t, db.Create(&clients[i]).Error)
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
UserID: user.ID,
ClientID: clients[i].ID,
}).Error)
}
withLaunchURL := utils.ListRequestOptions{
Filters: map[string][]any{"hasLaunchURL": {true}},
}
withoutLaunchURL := utils.ListRequestOptions{
Filters: map[string][]any{"hasLaunchURL": {false}},
}
allClients, allClientsPagination, err := s.ListAccessibleOidcClients(t.Context(), user.ID, utils.ListRequestOptions{})
require.NoError(t, err)
assert.Equal(t, int64(3), allClientsPagination.TotalItems)
assert.ElementsMatch(t, []string{"Launchable", "Missing launch URL", "Empty launch URL"}, accessibleClientNames(allClients))
launchableClients, launchablePagination, err := s.ListAccessibleOidcClients(t.Context(), user.ID, withLaunchURL)
require.NoError(t, err)
assert.Equal(t, int64(1), launchablePagination.TotalItems)
assert.Equal(t, []string{"Launchable"}, accessibleClientNames(launchableClients))
allAuthorizations, allAuthorizationsPagination, err := s.ListAuthorizedClients(t.Context(), user.ID, utils.ListRequestOptions{})
require.NoError(t, err)
assert.Equal(t, int64(3), allAuthorizationsPagination.TotalItems)
assert.Len(t, allAuthorizations, 3)
hiddenAuthorizations, hiddenPagination, err := s.ListAuthorizedClients(t.Context(), user.ID, withoutLaunchURL)
require.NoError(t, err)
assert.Equal(t, int64(2), hiddenPagination.TotalItems)
assert.ElementsMatch(t, []string{"Missing launch URL", "Empty launch URL"}, []string{
hiddenAuthorizations[0].Client.Name,
hiddenAuthorizations[1].Client.Name,
})
}
func accessibleClientNames(clients []dto.AccessibleOidcClientDto) []string {
names := make([]string, len(clients))
for i := range clients {