Skip to content

Commit

Permalink
[iOS] Admin Dashboard - API Keys (jellyfin#1284)
Browse files Browse the repository at this point in the history
* API Keys

* Switch Deletion Alert for a Confirmation Dialog

* Migrate from a list to a Collection VGrid.

* Convert back to List. Also, now using my events! So, there is a confirmation and a failure message for both delete & create API.

* want vs wish

* Merge Issue Fixes

* Review Changes

* Reset newAPIName after creating a new API

* cleanup

---------

Co-authored-by: Ethan Pippin <[email protected]>
  • Loading branch information
JPKribs and LePips authored Oct 25, 2024
1 parent c46ee13 commit 56fa032
Show file tree
Hide file tree
Showing 8 changed files with 458 additions and 3 deletions.
7 changes: 7 additions & 0 deletions Shared/Coordinators/SettingsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
var addServerTaskTrigger = makeAddServerTaskTrigger
@Route(.push)
var serverLogs = makeServerLogs
@Route(.push)
var apiKeys = makeAPIKeys
// <- End of AdminDashboard Items

#if DEBUG
Expand Down Expand Up @@ -230,6 +232,11 @@ final class SettingsCoordinator: NavigationCoordinatable {
ServerLogsView()
}

@ViewBuilder
func makeAPIKeys() -> some View {
APIKeysView()
}

// <- End of AdminDashboard Items

#if DEBUG
Expand Down
28 changes: 25 additions & 3 deletions Shared/Strings/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ internal enum L10n {
internal static let activeDevices = L10n.tr("Localizable", "activeDevices", fallback: "Active Devices")
/// Add
internal static let add = L10n.tr("Localizable", "add", fallback: "Add")
/// Add API key
internal static let addAPIKey = L10n.tr("Localizable", "addAPIKey", fallback: "Add API key")
/// Select Server View - Add Server
internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server")
/// Add trigger
Expand All @@ -46,10 +48,22 @@ internal enum L10n {
internal static let allServers = L10n.tr("Localizable", "allServers", fallback: "All Servers")
/// TranscodeReason - Anamorphic Video Not Supported
internal static let anamorphicVideoNotSupported = L10n.tr("Localizable", "anamorphicVideoNotSupported", fallback: "Anamorphic video is not supported")
/// API Key Copied
internal static let apiKeyCopied = L10n.tr("Localizable", "apiKeyCopied", fallback: "API Key Copied")
/// Your API Key was copied to your clipboard!
internal static let apiKeyCopiedMessage = L10n.tr("Localizable", "apiKeyCopiedMessage", fallback: "Your API Key was copied to your clipboard!")
/// API Keys
internal static let apiKeys = L10n.tr("Localizable", "apiKeys", fallback: "API Keys")
/// External applications require an API key to communicate with your server.
internal static let apiKeysDescription = L10n.tr("Localizable", "apiKeysDescription", fallback: "External applications require an API key to communicate with your server.")
/// API Keys
internal static let apiKeysTitle = L10n.tr("Localizable", "apiKeysTitle", fallback: "API Keys")
/// Represents the Appearance setting label
internal static let appearance = L10n.tr("Localizable", "appearance", fallback: "Appearance")
/// App Icon
internal static let appIcon = L10n.tr("Localizable", "appIcon", fallback: "App Icon")
/// Application Name
internal static let applicationName = L10n.tr("Localizable", "applicationName", fallback: "Application Name")
/// Apply
internal static let apply = L10n.tr("Localizable", "apply", fallback: "Apply")
/// Aspect Fill
Expand Down Expand Up @@ -218,6 +232,10 @@ internal enum L10n {
internal static let `continue` = L10n.tr("Localizable", "continue", fallback: "Continue")
/// Continue Watching
internal static let continueWatching = L10n.tr("Localizable", "continueWatching", fallback: "Continue Watching")
/// Create API Key
internal static let createAPIKey = L10n.tr("Localizable", "createAPIKey", fallback: "Create API Key")
/// Enter the application name for the new API key.
internal static let createAPIKeyMessage = L10n.tr("Localizable", "createAPIKeyMessage", fallback: "Enter the application name for the new API key.")
/// Current
internal static let current = L10n.tr("Localizable", "current", fallback: "Current")
/// Current Position
Expand Down Expand Up @@ -248,14 +266,18 @@ internal enum L10n {
internal static let dashboard = L10n.tr("Localizable", "dashboard", fallback: "Dashboard")
/// Description for the dashboard section
internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.")
/// Date Created
internal static let dateCreated = L10n.tr("Localizable", "dateCreated", fallback: "Date Created")
/// Day of Week
internal static let dayOfWeek = L10n.tr("Localizable", "dayOfWeek", fallback: "Day of Week")
/// Time Interval Help Text - Days
internal static let days = L10n.tr("Localizable", "days", fallback: "Days")
/// Default Scheme
internal static let defaultScheme = L10n.tr("Localizable", "defaultScheme", fallback: "Default Scheme")
/// Server Detail View - Delete
/// Delete
internal static let delete = L10n.tr("Localizable", "delete", fallback: "Delete")
/// Are you sure you want to permanently delete this key?
internal static let deleteAPIKeyMessage = L10n.tr("Localizable", "deleteAPIKeyMessage", fallback: "Are you sure you want to permanently delete this key?")
/// Delete Device
internal static let deleteDevice = L10n.tr("Localizable", "deleteDevice", fallback: "Delete Device")
/// Failed to Delete Device
Expand Down Expand Up @@ -544,8 +566,8 @@ internal enum L10n {
internal static let noTitle = L10n.tr("Localizable", "noTitle", fallback: "No title")
/// Video Player Settings View - Offset
internal static let offset = L10n.tr("Localizable", "offset", fallback: "Offset")
/// Ok
internal static let ok = L10n.tr("Localizable", "ok", fallback: "Ok")
/// OK
internal static let ok = L10n.tr("Localizable", "ok", fallback: "OK")
/// On application startup
internal static let onApplicationStartup = L10n.tr("Localizable", "onApplicationStartup", fallback: "On application startup")
/// 1 user
Expand Down
119 changes: 119 additions & 0 deletions Shared/ViewModels/APIKeysViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Combine
import Foundation
import JellyfinAPI
import OrderedCollections

// TODO: for APIKey updating, could temp set new APIKeys

final class APIKeysViewModel: ViewModel, Stateful {

// MARK: Action

enum Action: Equatable {
case getAPIKeys
case createAPIKey(name: String)
case deleteAPIKey(key: String)
}

// MARK: State

enum State: Hashable {
case initial
case error(JellyfinAPIError)
case content
}

// MARK: Published Variables

@Published
final var apiKeys: [AuthenticationInfo] = []
@Published
final var state: State = .initial

// MARK: Action Responses

func respond(to action: Action) -> State {
switch action {
case .getAPIKeys:
Task {
do {
try await getAPIKeys()

await MainActor.run {
self.state = .content
}
} catch {
await MainActor.run {
self.state = .error(.init(error.localizedDescription))
}
}
}
.store(in: &cancellables)
case let .createAPIKey(name):
Task {
do {
try await createAPIKey(name: name)

await MainActor.run {
self.state = .content
}
} catch {
await MainActor.run {
self.state = .error(.init(error.localizedDescription))
}
}
}
.store(in: &cancellables)
case let .deleteAPIKey(key):
Task {
do {
try await deleteAPIKey(key: key)

await MainActor.run {
self.state = .content
}
} catch {
await MainActor.run {
self.state = .error(.init(error.localizedDescription))
}
}
}
.store(in: &cancellables)
}

return state
}

private func getAPIKeys() async throws {
let request = Paths.getKeys
let response = try await userSession.client.send(request)

guard let items = response.value.items else { return }

await MainActor.run {
self.apiKeys = items
}
}

private func createAPIKey(name: String) async throws {
let request = Paths.createKey(app: name)
try await userSession.client.send(request).value

try await getAPIKeys()
}

private func deleteAPIKey(key: String) async throws {
let request = Paths.revokeKey(key: key)
try await userSession.client.send(request)

try await getAPIKeys()
}
}
30 changes: 30 additions & 0 deletions Swiftfin.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
4E35CE6A2CBED95F00DBD886 /* DayOfWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */; };
4E35CE6C2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; };
4E35CE6D2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; };
4E36395B2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */; };
4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */; };
4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; };
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; };
4E63B9FA2C8A5BEF00C25378 /* UserDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */; };
Expand All @@ -81,6 +83,8 @@
4E9A24E92C82B79D0023DA83 /* EditCustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */; };
4E9A24EB2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */; };
4E9A24ED2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24EC2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift */; };
4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA09DE02CC4E4F000CB27E4 /* APIKeysView.swift */; };
4EA09DE42CC4E85C00CB27E4 /* APIKeysRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */; };
4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; };
4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; };
4EB1A8CC2C9B1BA200F43898 /* DestructiveServerTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */; };
Expand Down Expand Up @@ -1090,6 +1094,7 @@
4E35CE652CBED8B300DBD886 /* ServerTicks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTicks.swift; sourceTree = "<group>"; };
4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeek.swift; sourceTree = "<group>"; };
4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskState.swift; sourceTree = "<group>"; };
4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysViewModel.swift; sourceTree = "<group>"; };
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDashboardView.swift; sourceTree = "<group>"; };
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = "<group>"; };
Expand All @@ -1110,6 +1115,8 @@
4E9A24E72C82B6190023DA83 /* CustomProfileButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProfileButton.swift; sourceTree = "<group>"; };
4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileCoordinator.swift; sourceTree = "<group>"; };
4E9A24EC2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomDeviceProfileView.swift; sourceTree = "<group>"; };
4EA09DE02CC4E4F000CB27E4 /* APIKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysView.swift; sourceTree = "<group>"; };
4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysRow.swift; sourceTree = "<group>"; };
4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = "<group>"; };
4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = "<group>"; };
4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveServerTask.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1988,6 +1995,7 @@
children = (
4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */,
4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */,
4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */,
4E35CE5B2CBED3F300DBD886 /* AddTaskTriggerView */,
E1DE64902CC6F06C00E423B6 /* Components */,
4E10C80F2CC030B20012CC9F /* DeviceDetailsView */,
Expand Down Expand Up @@ -2108,6 +2116,23 @@
path = Components;
sourceTree = "<group>";
};
4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */ = {
isa = PBXGroup;
children = (
4EA09DE22CC4E7BE00CB27E4 /* Components */,
4EA09DE02CC4E4F000CB27E4 /* APIKeysView.swift */,
);
path = APIKeyView;
sourceTree = "<group>";
};
4EA09DE22CC4E7BE00CB27E4 /* Components */ = {
isa = PBXGroup;
children = (
4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */,
);
path = Components;
sourceTree = "<group>";
};
4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2206,6 +2231,7 @@
isa = PBXGroup;
children = (
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */,
4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */,
E10231462BCF8A6D009D71FC /* ChannelLibraryViewModel.swift */,
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */,
Expand Down Expand Up @@ -4767,6 +4793,7 @@
E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
E185920A28CEF23A00326F80 /* FocusGuide.swift in Sources */,
E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */,
4E36395B2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */,
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
E102312F2BCF8A08009D71FC /* tvOSLiveTVCoordinator.swift in Sources */,
E1575E68293E77B5001665B1 /* LibraryParent.swift in Sources */,
Expand Down Expand Up @@ -5064,6 +5091,7 @@
6334175B287DDFB9000603CE /* QuickConnectAuthorizeView.swift in Sources */,
4E0A8FFB2CAF74D20014B047 /* TaskCompletionStatus.swift in Sources */,
E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */,
4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */,
E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
E11562952C818CB2001D5DE4 /* BindingBox.swift in Sources */,
4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */,
Expand Down Expand Up @@ -5267,6 +5295,7 @@
E1D8429329340B8300D1041A /* Utilities.swift in Sources */,
E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */,
E1D5C39628DF90C100CDBEFB /* Slider.swift in Sources */,
4EA09DE42CC4E85C00CB27E4 /* APIKeysRow.swift in Sources */,
E187A60229AB28F0008387E6 /* RotateContentView.swift in Sources */,
BD3957792C113EC40078CEF8 /* SubtitleSection.swift in Sources */,
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
Expand Down Expand Up @@ -5343,6 +5372,7 @@
4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */,
E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */,
4EC1C8522C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */,
4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */,
DFB7C3DF2C7AA43A00CE7CDC /* UserSignInState.swift in Sources */,
E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */,
4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */,
Expand Down
Loading

0 comments on commit 56fa032

Please sign in to comment.