mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-11-23 05:59:51 +00:00
[iOS] Admin Dashboard - User Passwords (#1312)
* resetUserPassword Adjustments * Nest the Password in Advanced because I dunno it looks nicer. * Dismiss Coordinator instead of pop. * Build issues * Rename my local xcode to xcode_16??? * Build plz * Comments * clean up --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
4dc8a31d6d
commit
128381a439
@ -37,8 +37,8 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||
var users = makeUsers
|
||||
@Route(.push)
|
||||
var userDetails = makeUserDetails
|
||||
@Route(.push)
|
||||
var userDevices = makeUserDevices
|
||||
@Route(.modal)
|
||||
var resetUserPassword = makeResetUserPassword
|
||||
@Route(.modal)
|
||||
var addServerUser = makeAddServerUser
|
||||
@Route(.push)
|
||||
@ -106,9 +106,10 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeUserDevices() -> some View {
|
||||
DevicesView()
|
||||
func makeResetUserPassword(userID: String) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator {
|
||||
ResetUserPasswordView(userID: userID, requiresCurrentPassword: false)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -26,7 +26,7 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
var playbackQualitySettings = makePlaybackQualitySettings
|
||||
@Route(.push)
|
||||
var quickConnect = makeQuickConnectAuthorize
|
||||
@Route(.push)
|
||||
@Route(.modal)
|
||||
var resetUserPassword = makeResetUserPassword
|
||||
@Route(.push)
|
||||
var localSecurity = makeLocalSecurity
|
||||
@ -112,9 +112,10 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
QuickConnectAuthorizeView()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeResetUserPassword() -> some View {
|
||||
ResetUserPasswordView()
|
||||
func makeResetUserPassword(userID: String) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
|
||||
NavigationViewCoordinator {
|
||||
ResetUserPasswordView(userID: userID, requiresCurrentPassword: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -220,6 +220,8 @@ internal enum L10n {
|
||||
internal static let confirm = L10n.tr("Localizable", "confirm", fallback: "Confirm")
|
||||
/// Confirm Close
|
||||
internal static let confirmClose = L10n.tr("Localizable", "confirmClose", fallback: "Confirm Close")
|
||||
/// Confirm New Password
|
||||
internal static let confirmNewPassword = L10n.tr("Localizable", "confirmNewPassword", fallback: "Confirm New Password")
|
||||
/// Confirm Password
|
||||
internal static let confirmPassword = L10n.tr("Localizable", "confirmPassword", fallback: "Confirm Password")
|
||||
/// Connect
|
||||
@ -250,6 +252,8 @@ internal enum L10n {
|
||||
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 Password
|
||||
internal static let currentPassword = L10n.tr("Localizable", "currentPassword", fallback: "Current Password")
|
||||
/// Current Position
|
||||
internal static let currentPosition = L10n.tr("Localizable", "currentPosition", fallback: "Current Position")
|
||||
/// PlaybackCompatibility Custom Category
|
||||
@ -550,6 +554,8 @@ internal enum L10n {
|
||||
internal static let never = L10n.tr("Localizable", "never", fallback: "Never")
|
||||
/// Message shown when a task has never run
|
||||
internal static let neverRun = L10n.tr("Localizable", "neverRun", fallback: "Never run")
|
||||
/// New Password
|
||||
internal static let newPassword = L10n.tr("Localizable", "newPassword", fallback: "New Password")
|
||||
/// News
|
||||
internal static let news = L10n.tr("Localizable", "news", fallback: "News")
|
||||
/// New User
|
||||
@ -636,8 +642,12 @@ internal enum L10n {
|
||||
}
|
||||
/// Password
|
||||
internal static let password = L10n.tr("Localizable", "password", fallback: "Password")
|
||||
/// New passwords do not match
|
||||
internal static let passwordsDoNotMatch = L10n.tr("Localizable", "passwordsDoNotMatch", fallback: "New passwords do not match")
|
||||
/// User password has been changed.
|
||||
internal static let passwordChangedMessage = L10n.tr("Localizable", "passwordChangedMessage", fallback: "User password has been changed.")
|
||||
/// Changes the Jellyfin server user password. This does not change any Swiftfin settings.
|
||||
internal static let passwordChangeWarning = L10n.tr("Localizable", "passwordChangeWarning", fallback: "Changes the Jellyfin server user password. This does not change any Swiftfin settings.")
|
||||
/// New passwords do not match.
|
||||
internal static let passwordsDoNotMatch = L10n.tr("Localizable", "passwordsDoNotMatch", fallback: "New passwords do not match.")
|
||||
/// Video Player Settings View - Pause on Background
|
||||
internal static let pauseOnBackground = L10n.tr("Localizable", "pauseOnBackground", fallback: "Pause on background")
|
||||
/// People
|
||||
|
@ -12,29 +12,32 @@ import JellyfinAPI
|
||||
|
||||
final class ResetUserPasswordViewModel: ViewModel, Eventful, Stateful {
|
||||
|
||||
// MARK: Event
|
||||
// MARK: - Event
|
||||
|
||||
enum Event {
|
||||
case error(JellyfinAPIError)
|
||||
case success
|
||||
}
|
||||
|
||||
// MARK: Action
|
||||
// MARK: - Action
|
||||
|
||||
enum Action: Equatable {
|
||||
case cancel
|
||||
case reset(current: String, new: String)
|
||||
}
|
||||
|
||||
// MARK: State
|
||||
// MARK: - State
|
||||
|
||||
enum State: Hashable {
|
||||
case initial
|
||||
case resetting
|
||||
}
|
||||
|
||||
// MARK: - Published Variables
|
||||
|
||||
@Published
|
||||
var state: State = .initial
|
||||
let userID: String
|
||||
|
||||
var events: AnyPublisher<Event, Never> {
|
||||
eventSubject
|
||||
@ -45,6 +48,12 @@ final class ResetUserPasswordViewModel: ViewModel, Eventful, Stateful {
|
||||
private var resetTask: AnyCancellable?
|
||||
private var eventSubject: PassthroughSubject<Event, Never> = .init()
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
init(userID: String) {
|
||||
self.userID = userID
|
||||
}
|
||||
|
||||
func respond(to action: Action) -> State {
|
||||
switch action {
|
||||
case .cancel:
|
||||
@ -79,7 +88,7 @@ final class ResetUserPasswordViewModel: ViewModel, Eventful, Stateful {
|
||||
|
||||
private func reset(current: String, new: String) async throws {
|
||||
let body = UpdateUserPassword(currentPw: current, newPw: new)
|
||||
let request = Paths.updateUserPassword(userID: userSession.user.id, body)
|
||||
let request = Paths.updateUserPassword(userID: userID, body)
|
||||
|
||||
try await userSession.client.send(request)
|
||||
}
|
||||
|
@ -2273,6 +2273,14 @@
|
||||
path = DevicesView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EF10D4C2CE2EC5A000ED5F5 /* ResetUserPasswordView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1545BD72BDC55C300D9578F /* ResetUserPasswordView.swift */,
|
||||
);
|
||||
path = ResetUserPasswordView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EF18B232CB9932F00343666 /* PagingLibraryView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -3295,6 +3303,7 @@
|
||||
E1EDA8D62B92C9D700F9A57E /* PagingLibraryView */,
|
||||
E10231342BCF8A3C009D71FC /* ProgramsView */,
|
||||
E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */,
|
||||
4EF10D4C2CE2EC5A000ED5F5 /* ResetUserPasswordView */,
|
||||
53EE24E5265060780068F029 /* SearchView.swift */,
|
||||
E10B1EAF2BD9769500A92EAF /* SelectUserView */,
|
||||
E19D41AB2BF288110082B8B2 /* ServerCheckView.swift */,
|
||||
@ -3387,7 +3396,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6334175A287DDFB9000603CE /* QuickConnectAuthorizeView.swift */,
|
||||
E1545BD72BDC55C300D9578F /* ResetUserPasswordView.swift */,
|
||||
E1EA09872BEE9CF3004CDE76 /* UserLocalSecurityView.swift */,
|
||||
E14EA1612BF6FF8D00DE757A /* UserProfileImagePicker */,
|
||||
E1BE1CEF2BDB6C97008176A9 /* UserProfileSettingsView.swift */,
|
||||
|
@ -35,6 +35,15 @@ struct ServerUserDetailsView: View {
|
||||
user: viewModel.user,
|
||||
lastActivityDate: viewModel.user.lastActivityDate
|
||||
)
|
||||
|
||||
Section(L10n.advanced) {
|
||||
if let userId = viewModel.user.id {
|
||||
ChevronButton(L10n.password)
|
||||
.onSelect {
|
||||
router.route(to: \.resetUserPassword, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.user)
|
||||
.onAppear {
|
||||
|
@ -13,14 +13,25 @@ import SwiftUI
|
||||
|
||||
struct ResetUserPasswordView: View {
|
||||
|
||||
private enum Field: Hashable {
|
||||
case currentPassword
|
||||
case newPassword
|
||||
case confirmNewPassword
|
||||
}
|
||||
|
||||
@Default(.accentColor)
|
||||
private var accentColor
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: SettingsCoordinator.Router
|
||||
private var router: BasicNavigationViewCoordinator.Router
|
||||
|
||||
@FocusState
|
||||
private var focusedPassword: Int?
|
||||
private var focusedField: Field?
|
||||
|
||||
@StateObject
|
||||
private var viewModel: ResetUserPasswordViewModel
|
||||
|
||||
// MARK: - Password Variables
|
||||
|
||||
@State
|
||||
private var currentPassword: String = ""
|
||||
@ -29,6 +40,8 @@ struct ResetUserPasswordView: View {
|
||||
@State
|
||||
private var confirmNewPassword: String = ""
|
||||
|
||||
// MARK: - State Variables
|
||||
|
||||
@State
|
||||
private var error: Error? = nil
|
||||
@State
|
||||
@ -36,45 +49,54 @@ struct ResetUserPasswordView: View {
|
||||
@State
|
||||
private var isPresentingSuccess: Bool = false
|
||||
|
||||
@StateObject
|
||||
private var viewModel = ResetUserPasswordViewModel()
|
||||
private let requiresCurrentPassword: Bool
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
init(userID: String, requiresCurrentPassword: Bool) {
|
||||
self._viewModel = StateObject(wrappedValue: ResetUserPasswordViewModel(userID: userID))
|
||||
self.requiresCurrentPassword = requiresCurrentPassword
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
|
||||
Section("Current Password") {
|
||||
UnmaskSecureField("Current Password", text: $currentPassword) {
|
||||
focusedPassword = 1
|
||||
if requiresCurrentPassword {
|
||||
Section(L10n.currentPassword) {
|
||||
UnmaskSecureField(L10n.currentPassword, text: $currentPassword) {
|
||||
focusedField = .newPassword
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.none)
|
||||
.focused($focusedField, equals: .currentPassword)
|
||||
.disabled(viewModel.state == .resetting)
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.none)
|
||||
.focused($focusedPassword, equals: 0)
|
||||
.disabled(viewModel.state == .resetting)
|
||||
}
|
||||
|
||||
Section("New Password") {
|
||||
UnmaskSecureField("New Password", text: $newPassword) {
|
||||
focusedPassword = 2
|
||||
Section(L10n.newPassword) {
|
||||
UnmaskSecureField(L10n.newPassword, text: $newPassword) {
|
||||
focusedField = .confirmNewPassword
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.none)
|
||||
.focused($focusedPassword, equals: 1)
|
||||
.focused($focusedField, equals: .newPassword)
|
||||
.disabled(viewModel.state == .resetting)
|
||||
}
|
||||
|
||||
Section {
|
||||
UnmaskSecureField("Confirm New Password", text: $confirmNewPassword) {
|
||||
UnmaskSecureField(L10n.confirmNewPassword, text: $confirmNewPassword) {
|
||||
viewModel.send(.reset(current: currentPassword, new: confirmNewPassword))
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.none)
|
||||
.focused($focusedPassword, equals: 2)
|
||||
.focused($focusedField, equals: .confirmNewPassword)
|
||||
.disabled(viewModel.state == .resetting)
|
||||
} header: {
|
||||
Text("Confirm New Password")
|
||||
Text(L10n.confirmNewPassword)
|
||||
} footer: {
|
||||
if newPassword != confirmNewPassword {
|
||||
Label("New passwords to not match", systemImage: "exclamationmark.circle.fill")
|
||||
Label(L10n.passwordsDoNotMatch, systemImage: "exclamationmark.circle.fill")
|
||||
.labelStyle(.sectionFooterWithImage(imageStyle: .orange))
|
||||
}
|
||||
}
|
||||
@ -83,12 +105,17 @@ struct ResetUserPasswordView: View {
|
||||
if viewModel.state == .resetting {
|
||||
ListRowButton(L10n.cancel) {
|
||||
viewModel.send(.cancel)
|
||||
focusedPassword = 0
|
||||
|
||||
if requiresCurrentPassword {
|
||||
focusedField = .currentPassword
|
||||
} else {
|
||||
focusedField = .newPassword
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.red, .red.opacity(0.2))
|
||||
} else {
|
||||
ListRowButton(L10n.save) {
|
||||
focusedPassword = nil
|
||||
focusedField = nil
|
||||
viewModel.send(.reset(current: currentPassword, new: confirmNewPassword))
|
||||
}
|
||||
.disabled(newPassword != confirmNewPassword || viewModel.state == .resetting)
|
||||
@ -96,14 +123,22 @@ struct ResetUserPasswordView: View {
|
||||
.opacity(newPassword != confirmNewPassword ? 0.5 : 1)
|
||||
}
|
||||
} footer: {
|
||||
Text("Changes the Jellyfin server user password. This does not change any Swiftfin settings.")
|
||||
Text(L10n.passwordChangeWarning)
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled(viewModel.state == .resetting)
|
||||
.navigationBarBackButtonHidden(viewModel.state == .resetting)
|
||||
.navigationTitle(L10n.password)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarCloseButton {
|
||||
router.dismissCoordinator()
|
||||
}
|
||||
.onFirstAppear {
|
||||
focusedPassword = 0
|
||||
if requiresCurrentPassword {
|
||||
focusedField = .currentPassword
|
||||
} else {
|
||||
focusedField = .newPassword
|
||||
}
|
||||
}
|
||||
.onReceive(viewModel.events) { event in
|
||||
switch event {
|
||||
@ -124,12 +159,12 @@ struct ResetUserPasswordView: View {
|
||||
}
|
||||
}
|
||||
.alert(
|
||||
L10n.error.text,
|
||||
L10n.error,
|
||||
isPresented: $isPresentingError,
|
||||
presenting: error
|
||||
) { _ in
|
||||
Button(L10n.dismiss, role: .cancel) {
|
||||
focusedPassword = 1
|
||||
focusedField = .newPassword
|
||||
}
|
||||
} message: { error in
|
||||
Text(error.localizedDescription)
|
||||
@ -139,10 +174,10 @@ struct ResetUserPasswordView: View {
|
||||
isPresented: $isPresentingSuccess
|
||||
) {
|
||||
Button(L10n.dismiss, role: .cancel) {
|
||||
router.pop()
|
||||
router.dismissCoordinator()
|
||||
}
|
||||
} message: {
|
||||
Text("User password has been changed.")
|
||||
Text(L10n.passwordChangedMessage)
|
||||
}
|
||||
}
|
||||
}
|
@ -91,7 +91,7 @@ struct UserProfileSettingsView: View {
|
||||
|
||||
ChevronButton("Password")
|
||||
.onSelect {
|
||||
router.route(to: \.resetUserPassword)
|
||||
router.route(to: \.resetUserPassword, viewModel.userSession.user.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,6 @@
|
||||
"ok" = "Ok";
|
||||
"otherUser" = "Other User";
|
||||
"pageOfWithNumbers" = "Page %1$@ of %2$@";
|
||||
"password" = "Password";
|
||||
"playNext" = "Play Next";
|
||||
"play" = "Play";
|
||||
"playback" = "Playback";
|
||||
@ -1213,3 +1212,38 @@
|
||||
// Button title to edit existing users
|
||||
// Used as the button label in the options menu when there are users to edit
|
||||
"editUsers" = "Edit Users";
|
||||
|
||||
/// Current Password - Placeholder
|
||||
/// Placeholder text for the current password input field
|
||||
/// Used in the ResetUserPasswordView
|
||||
"currentPassword" = "Current Password";
|
||||
|
||||
/// New Password - Placeholder
|
||||
/// Placeholder text for the new password input field
|
||||
/// Used in the ResetUserPasswordView
|
||||
"newPassword" = "New Password";
|
||||
|
||||
/// Confirm New Password - Placeholder
|
||||
/// Placeholder text for confirming the new password input field
|
||||
/// Used in the ResetUserPasswordView
|
||||
"confirmNewPassword" = "Confirm New Password";
|
||||
|
||||
/// Password - Navigation Title
|
||||
/// Title for the password reset view
|
||||
/// Used in the navigation bar
|
||||
"password" = "Password";
|
||||
|
||||
/// Password Changed - Alert Message
|
||||
/// Message displayed in the success alert after changing the password
|
||||
/// Used in the ResetUserPasswordView
|
||||
"passwordChangedMessage" = "User password has been changed.";
|
||||
|
||||
/// Passwords Do Not Match - Footer
|
||||
/// Error message displayed when new passwords do not match
|
||||
/// Used in the ResetUserPasswordView
|
||||
"passwordsDoNotMatch" = "New passwords do not match.";
|
||||
|
||||
/// Password Change Warning - Message
|
||||
/// Message displayed to alert the user what the password change does and does not do
|
||||
/// Used in the ResetUserPasswordView
|
||||
"passwordChangeWarning" = "Changes the Jellyfin server user password. This does not change any Swiftfin settings.";
|
||||
|
Loading…
Reference in New Issue
Block a user