Create password attempt when submitting

This commit is contained in:
Markus Thielker 2025-01-18 00:33:45 +01:00
parent 54b1cc1884
commit f766e0893a
No known key found for this signature in database
7 changed files with 183 additions and 47 deletions

View file

@ -12,6 +12,7 @@ import SwiftData
class PasswordManager {
let passwordRepository: PasswordRepository
let passwordAttemptRepository: PasswordAttemptRepository
let passwordKeychainRepository: PasswordKeychainRepository
private let context: ModelContext
@ -19,10 +20,12 @@ class PasswordManager {
init(
context: ModelContext,
passwordRepository: PasswordRepository,
passwordAttemptRepository: PasswordAttemptRepository,
passwordKeychainRepository: PasswordKeychainRepository
) {
self.context = context
self.passwordRepository = passwordRepository
self.passwordAttemptRepository = passwordAttemptRepository
self.passwordKeychainRepository = passwordKeychainRepository
}
@ -40,6 +43,10 @@ class PasswordManager {
return passwordKeychainRepository.getPassword(withID: id)
}
func getPasswordAttempts(passwordID id: UUID) -> [PasswordAttempt] {
return passwordAttemptRepository.getPasswordAttempts(passwordID: id)
}
@MainActor
func createPassword(name: String, value: String) -> Password {
@ -65,6 +72,16 @@ class PasswordManager {
return password
}
@MainActor
func createPasswordAttempt(passwordID: UUID, isSuccessful: Bool, typingTime: Double) {
let attempt = PasswordAttempt(
password: passwordID,
isSuccessful: isSuccessful,
typingTime: typingTime
)
passwordAttemptRepository.createPasswordAttempt(attempt: attempt)
}
@MainActor
func deletePassword(_ password: Password) -> Bool {
@ -76,6 +93,7 @@ class PasswordManager {
try context.transaction {
passwordRepository.deletePassword(password)
try passwordAttemptRepository.deleteAllAttempts(withID: password.id)
try passwordKeychainRepository.deletePassword(withID: password.id)
}

View file

@ -0,0 +1,54 @@
//
// PasswordAttemptRepository.swift
// password
//
// Created by Markus Thielker on 17.01.25.
//
import Foundation
import SwiftUICore
import SwiftData
class PasswordAttemptRepository {
private let context: ModelContext
init(_ context: ModelContext) {
self.context = context
}
func getPasswordAttempts(passwordID id: UUID) -> [PasswordAttempt] {
print("fetching attempts for password \(id)")
var attempts: [PasswordAttempt] = []
do {
let request = FetchDescriptor<PasswordAttempt>(
predicate: #Predicate { $0.password == id },
sortBy: [SortDescriptor(\.timestamp)]
)
attempts = try context.fetch(request)
print("found \(attempts.count) attempts for password \(id)")
} catch {
print("fetching attempts failed: \(error)")
}
return attempts
}
@MainActor
func createPasswordAttempt(attempt: PasswordAttempt) {
context.insert(attempt)
print("inserted attempt")
}
@MainActor
func deleteAllAttempts(withID id: UUID) throws {
let predicate = #Predicate<PasswordAttempt> { $0.password == id }
try context.delete(model: PasswordAttempt.self, where: predicate)
print("deleted all attempts for password \(id)")
}
}

View file

@ -9,10 +9,6 @@ import Foundation
import SwiftUICore
import SwiftData
enum PasswordRepositoryError: Error {
case notFound
}
class PasswordRepository {
private let context: ModelContext

View file

@ -9,47 +9,61 @@ import SwiftUI
struct DetailView: View {
let password: Password
let passwordKC: PasswordKC
@ObservedObject var viewModel: DetailViewModel
@State var value: String = ""
func validateInput(input: String, password: Password) -> Bool {
return input == passwordKC.value
@State private var startTime: Date?
@State private var elapsedTime: Double = -1
@State private var value: String = ""
init(viewModel: DetailViewModel) {
self.viewModel = viewModel
}
func validateInput(input: String, password: Password) -> Bool {
return input == viewModel.passwordKC.value
}
var body: some View {
VStack {
Text("Enter the password for \(password.name)")
Text("Enter the password for \(viewModel.password.name) and submit with \"Enter\"")
Form {
SecureField("", text: $value)
Button("Submit") {
let correct = validateInput(input: value, password: password)
if (correct) {
let alert = NSAlert()
alert.messageText = "Correct"
alert.informativeText = "That one was correct!"
alert.addButton(withTitle: "Let's go!")
alert.runModal()
} else {
let alert = NSAlert()
alert.messageText = "Not quite"
alert.informativeText = " That one was not quite right! Try again!"
alert.addButton(withTitle: "Okay")
alert.runModal()
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: value) { _, _ in
if (value.isEmpty){
startTime = nil
} else if startTime == nil {
startTime = Date()
}
}
.onSubmit {
if let startTime = startTime {
elapsedTime = Date().timeIntervalSince(startTime)
}
let isSuccessful = validateInput(input: value, password: viewModel.password)
viewModel.createPasswordAttempt(isSuccessful: isSuccessful, typingTime: elapsedTime)
if isSuccessful {
let alert = NSAlert()
alert.messageText = "Correct"
alert.informativeText = "That one was correct!"
alert.addButton(withTitle: "Let's go!")
alert.runModal()
} else {
let alert = NSAlert()
alert.messageText = "Not quite"
alert.informativeText = " That one was not quite right! Try again!"
alert.addButton(withTitle: "Okay")
alert.runModal()
}
value = ""
startTime = nil
elapsedTime = -1
}
value = ""
}
}
}
.padding()
}
}
#Preview {
let password = Password(name: "macbook")
let passwordKC = PasswordKC(id: password.id, value: "admin")
DetailView(password: password, passwordKC: passwordKC)
}

View file

@ -0,0 +1,58 @@
//
// ListViewModel.swift
// password
//
// Created by Markus Thielker on 16.01.25.
//
import Foundation
import SwiftData
class DetailViewModel: ObservableObject {
let passwordID: UUID
private let context: ModelContext
private let passwordManager: PasswordManager
@Published var password: Password
@Published var passwordKC: PasswordKC
@Published var passwordAttempts: [PasswordAttempt]
@MainActor
init(context: ModelContext, passwordID id: UUID) {
self.context = context
self.passwordID = id
let passwordRepository = PasswordRepository(context)
let passwordAttemptRepository = PasswordAttemptRepository(context)
let passwordKeychainRepository = PasswordKeychainRepository()
self.passwordManager = PasswordManager(
context: context,
passwordRepository: passwordRepository,
passwordAttemptRepository: passwordAttemptRepository,
passwordKeychainRepository: passwordKeychainRepository
)
password = passwordManager.getPassword(withID: id)!
passwordKC = passwordManager.getPasswordKeychain(withID: id)!
passwordAttempts = passwordManager.getPasswordAttempts(passwordID: id)
}
@MainActor
func getPassword(withID id: UUID) -> Password? {
return passwordManager.getPassword(withID: id)
}
@MainActor
func getPasswordKeychain(withID id: UUID) -> PasswordKC? {
return passwordManager.getPasswordKeychain(withID: id)
}
@MainActor
func createPasswordAttempt(isSuccessful: Bool, typingTime: Double) {
passwordManager.createPasswordAttempt(passwordID: password.id, isSuccessful: isSuccessful, typingTime: typingTime)
passwordAttempts = passwordManager.getPasswordAttempts(passwordID: password.id)
}
}

View file

@ -10,6 +10,8 @@ import _SwiftData_SwiftUI
struct ListView: View {
@Environment(\.modelContext) var context
@ObservedObject var viewModel: ListViewModel
@State var isAddingPassword: Bool = false
@ -17,10 +19,7 @@ struct ListView: View {
NavigationView {
List {
ForEach(viewModel.passwords) { password in
NavigationLink(destination: DetailView(
password: password,
passwordKC: viewModel.getPasswordKeychain(withID: password.id)!
)) {
NavigationLink(destination: DetailView(viewModel: DetailViewModel(context: context, passwordID: password.id))) {
Text(password.name)
}
}
@ -32,7 +31,7 @@ struct ListView: View {
.frame(width: 20, height: 20)
}
Button(action: {
viewModel.getAllPasswords()
viewModel.passwords = viewModel.getAllPasswords()
}) {
Image(systemName: "arrow.trianglehead.clockwise")
.imageScale(.medium)
@ -45,7 +44,7 @@ struct ListView: View {
AddPasswordView(viewModel: viewModel)
}
.onAppear {
viewModel.getAllPasswords()
viewModel.passwords = viewModel.getAllPasswords()
}
}
}

View file

@ -20,15 +20,15 @@ class ListViewModel: ObservableObject {
self.context = context
let passwordRepository = PasswordRepository(context)
let passwordAttemptRepository = PasswordAttemptRepository(context)
let passwordKeychainRepository = PasswordKeychainRepository()
self.passwordManager = PasswordManager(
context: context,
passwordRepository: passwordRepository,
passwordAttemptRepository: passwordAttemptRepository,
passwordKeychainRepository: passwordKeychainRepository
)
passwords = getAllPasswords()
}
@MainActor
@ -55,8 +55,5 @@ class ListViewModel: ObservableObject {
@MainActor
func deletePassword(_ password: Password) {
let success = passwordManager.deletePassword(password)
if success {
passwords = getAllPasswords()
}
}
}