Create password attempt when submitting
This commit is contained in:
parent
54b1cc1884
commit
f766e0893a
7 changed files with 183 additions and 47 deletions
|
@ -12,6 +12,7 @@ import SwiftData
|
||||||
class PasswordManager {
|
class PasswordManager {
|
||||||
|
|
||||||
let passwordRepository: PasswordRepository
|
let passwordRepository: PasswordRepository
|
||||||
|
let passwordAttemptRepository: PasswordAttemptRepository
|
||||||
let passwordKeychainRepository: PasswordKeychainRepository
|
let passwordKeychainRepository: PasswordKeychainRepository
|
||||||
|
|
||||||
private let context: ModelContext
|
private let context: ModelContext
|
||||||
|
@ -19,10 +20,12 @@ class PasswordManager {
|
||||||
init(
|
init(
|
||||||
context: ModelContext,
|
context: ModelContext,
|
||||||
passwordRepository: PasswordRepository,
|
passwordRepository: PasswordRepository,
|
||||||
|
passwordAttemptRepository: PasswordAttemptRepository,
|
||||||
passwordKeychainRepository: PasswordKeychainRepository
|
passwordKeychainRepository: PasswordKeychainRepository
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.passwordRepository = passwordRepository
|
self.passwordRepository = passwordRepository
|
||||||
|
self.passwordAttemptRepository = passwordAttemptRepository
|
||||||
self.passwordKeychainRepository = passwordKeychainRepository
|
self.passwordKeychainRepository = passwordKeychainRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +43,10 @@ class PasswordManager {
|
||||||
return passwordKeychainRepository.getPassword(withID: id)
|
return passwordKeychainRepository.getPassword(withID: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPasswordAttempts(passwordID id: UUID) -> [PasswordAttempt] {
|
||||||
|
return passwordAttemptRepository.getPasswordAttempts(passwordID: id)
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func createPassword(name: String, value: String) -> Password {
|
func createPassword(name: String, value: String) -> Password {
|
||||||
|
|
||||||
|
@ -65,6 +72,16 @@ class PasswordManager {
|
||||||
return password
|
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
|
@MainActor
|
||||||
func deletePassword(_ password: Password) -> Bool {
|
func deletePassword(_ password: Password) -> Bool {
|
||||||
|
|
||||||
|
@ -76,6 +93,7 @@ class PasswordManager {
|
||||||
|
|
||||||
try context.transaction {
|
try context.transaction {
|
||||||
passwordRepository.deletePassword(password)
|
passwordRepository.deletePassword(password)
|
||||||
|
try passwordAttemptRepository.deleteAllAttempts(withID: password.id)
|
||||||
try passwordKeychainRepository.deletePassword(withID: password.id)
|
try passwordKeychainRepository.deletePassword(withID: password.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
54
password/data/repository/PasswordAttemptRepository.swift
Normal file
54
password/data/repository/PasswordAttemptRepository.swift
Normal 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)")
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,10 +9,6 @@ import Foundation
|
||||||
import SwiftUICore
|
import SwiftUICore
|
||||||
import SwiftData
|
import SwiftData
|
||||||
|
|
||||||
enum PasswordRepositoryError: Error {
|
|
||||||
case notFound
|
|
||||||
}
|
|
||||||
|
|
||||||
class PasswordRepository {
|
class PasswordRepository {
|
||||||
|
|
||||||
private let context: ModelContext
|
private let context: ModelContext
|
||||||
|
|
|
@ -9,24 +9,42 @@ import SwiftUI
|
||||||
|
|
||||||
struct DetailView: View {
|
struct DetailView: View {
|
||||||
|
|
||||||
let password: Password
|
@ObservedObject var viewModel: DetailViewModel
|
||||||
let passwordKC: PasswordKC
|
|
||||||
|
|
||||||
@State var value: String = ""
|
@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 {
|
func validateInput(input: String, password: Password) -> Bool {
|
||||||
return input == passwordKC.value
|
return input == viewModel.passwordKC.value
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Text("Enter the password for \(password.name)")
|
Text("Enter the password for \(viewModel.password.name) and submit with \"Enter\"")
|
||||||
Form {
|
Form {
|
||||||
SecureField("", text: $value)
|
SecureField("", text: $value)
|
||||||
Button("Submit") {
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
let correct = validateInput(input: value, password: password)
|
.onChange(of: value) { _, _ in
|
||||||
|
if (value.isEmpty){
|
||||||
|
startTime = nil
|
||||||
|
} else if startTime == nil {
|
||||||
|
startTime = Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onSubmit {
|
||||||
|
if let startTime = startTime {
|
||||||
|
elapsedTime = Date().timeIntervalSince(startTime)
|
||||||
|
}
|
||||||
|
|
||||||
if (correct) {
|
let isSuccessful = validateInput(input: value, password: viewModel.password)
|
||||||
|
viewModel.createPasswordAttempt(isSuccessful: isSuccessful, typingTime: elapsedTime)
|
||||||
|
|
||||||
|
if isSuccessful {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = "Correct"
|
alert.messageText = "Correct"
|
||||||
alert.informativeText = "That one was correct!"
|
alert.informativeText = "That one was correct!"
|
||||||
|
@ -41,15 +59,11 @@ struct DetailView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
value = ""
|
value = ""
|
||||||
|
startTime = nil
|
||||||
|
elapsedTime = -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
|
||||||
let password = Password(name: "macbook")
|
|
||||||
let passwordKC = PasswordKC(id: password.id, value: "admin")
|
|
||||||
DetailView(password: password, passwordKC: passwordKC)
|
|
||||||
}
|
|
||||||
|
|
58
password/view/detail/DetailViewModel.swift
Normal file
58
password/view/detail/DetailViewModel.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ import _SwiftData_SwiftUI
|
||||||
|
|
||||||
struct ListView: View {
|
struct ListView: View {
|
||||||
|
|
||||||
|
@Environment(\.modelContext) var context
|
||||||
|
|
||||||
@ObservedObject var viewModel: ListViewModel
|
@ObservedObject var viewModel: ListViewModel
|
||||||
@State var isAddingPassword: Bool = false
|
@State var isAddingPassword: Bool = false
|
||||||
|
|
||||||
|
@ -17,10 +19,7 @@ struct ListView: View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
List {
|
List {
|
||||||
ForEach(viewModel.passwords) { password in
|
ForEach(viewModel.passwords) { password in
|
||||||
NavigationLink(destination: DetailView(
|
NavigationLink(destination: DetailView(viewModel: DetailViewModel(context: context, passwordID: password.id))) {
|
||||||
password: password,
|
|
||||||
passwordKC: viewModel.getPasswordKeychain(withID: password.id)!
|
|
||||||
)) {
|
|
||||||
Text(password.name)
|
Text(password.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +31,7 @@ struct ListView: View {
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
}
|
}
|
||||||
Button(action: {
|
Button(action: {
|
||||||
viewModel.getAllPasswords()
|
viewModel.passwords = viewModel.getAllPasswords()
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: "arrow.trianglehead.clockwise")
|
Image(systemName: "arrow.trianglehead.clockwise")
|
||||||
.imageScale(.medium)
|
.imageScale(.medium)
|
||||||
|
@ -45,7 +44,7 @@ struct ListView: View {
|
||||||
AddPasswordView(viewModel: viewModel)
|
AddPasswordView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getAllPasswords()
|
viewModel.passwords = viewModel.getAllPasswords()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,15 +20,15 @@ class ListViewModel: ObservableObject {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
let passwordRepository = PasswordRepository(context)
|
let passwordRepository = PasswordRepository(context)
|
||||||
|
let passwordAttemptRepository = PasswordAttemptRepository(context)
|
||||||
let passwordKeychainRepository = PasswordKeychainRepository()
|
let passwordKeychainRepository = PasswordKeychainRepository()
|
||||||
|
|
||||||
self.passwordManager = PasswordManager(
|
self.passwordManager = PasswordManager(
|
||||||
context: context,
|
context: context,
|
||||||
passwordRepository: passwordRepository,
|
passwordRepository: passwordRepository,
|
||||||
|
passwordAttemptRepository: passwordAttemptRepository,
|
||||||
passwordKeychainRepository: passwordKeychainRepository
|
passwordKeychainRepository: passwordKeychainRepository
|
||||||
)
|
)
|
||||||
|
|
||||||
passwords = getAllPasswords()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
@ -55,8 +55,5 @@ class ListViewModel: ObservableObject {
|
||||||
@MainActor
|
@MainActor
|
||||||
func deletePassword(_ password: Password) {
|
func deletePassword(_ password: Password) {
|
||||||
let success = passwordManager.deletePassword(password)
|
let success = passwordManager.deletePassword(password)
|
||||||
if success {
|
|
||||||
passwords = getAllPasswords()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue