summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShav Kinderlehrer <[email protected]>2023-12-12 17:09:15 -0500
committerShav Kinderlehrer <[email protected]>2023-12-12 17:09:15 -0500
commitfbb37567460b689f01eb8a8717b9ac8673652c28 (patch)
treeb6517179cb849732efb8660edfa9899456420d3b
parent02fc87fe2588cdca5188cf1a6d338ce83de65a43 (diff)
downloadjel-fbb37567460b689f01eb8a8717b9ac8673652c28.tar.gz
jel-fbb37567460b689f01eb8a8717b9ac8673652c28.zip
Implement signIn flow
-rw-r--r--Jel.xcodeproj/project.pbxproj22
-rw-r--r--Jel/ContentView.swift3
-rw-r--r--Jel/Controllers/AuthStateController.swift2
-rw-r--r--Jel/Controllers/JellyfinClientController.swift66
-rw-r--r--Jel/JelApp.swift14
-rw-r--r--Jel/Models/JellyfinDateFormatter.swift33
-rw-r--r--Jel/Views/SignIn/AddServerView.swift (renamed from Jel/Views/AddServerView.swift)33
-rw-r--r--Jel/Views/SignIn/SignInToServerView.swift79
-rw-r--r--Jel/Views/SignIn/SignInView.swift42
9 files changed, 266 insertions, 28 deletions
diff --git a/Jel.xcodeproj/project.pbxproj b/Jel.xcodeproj/project.pbxproj
index 85470fb..cc85edc 100644
--- a/Jel.xcodeproj/project.pbxproj
+++ b/Jel.xcodeproj/project.pbxproj
@@ -19,6 +19,9 @@
3D9063E72B279A320063DD2A /* JelUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063E62B279A320063DD2A /* JelUITests.swift */; };
3D9063E92B279A320063DD2A /* JelUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063E82B279A320063DD2A /* JelUITestsLaunchTests.swift */; };
3D9064592B27E4C70063DD2A /* JellyfinKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3D9064582B27E4C70063DD2A /* JellyfinKit */; };
+ 3D91FDC92B28C62800919017 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDC82B28C62800919017 /* SignInView.swift */; };
+ 3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDCA2B28CA2500919017 /* SignInToServerView.swift */; };
+ 3D91FDCD2B2907E800919017 /* JellyfinDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */; };
3DF1ED3E2B282836000AD8EA /* JellyfinClientController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1ED3D2B282836000AD8EA /* JellyfinClientController.swift */; };
/* End PBXBuildFile section */
@@ -69,6 +72,9 @@
3D9063E22B279A320063DD2A /* JelUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JelUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3D9063E62B279A320063DD2A /* JelUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JelUITests.swift; sourceTree = "<group>"; };
3D9063E82B279A320063DD2A /* JelUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JelUITestsLaunchTests.swift; sourceTree = "<group>"; };
+ 3D91FDC82B28C62800919017 /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = "<group>"; };
+ 3D91FDCA2B28CA2500919017 /* SignInToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInToServerView.swift; sourceTree = "<group>"; };
+ 3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinDateFormatter.swift; sourceTree = "<group>"; };
3DC0E5802B2832B9001CCE96 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3DF1ED3D2B282836000AD8EA /* JellyfinClientController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinClientController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -102,7 +108,7 @@
3D1015D72B27F54A00F5C29A /* Views */ = {
isa = PBXGroup;
children = (
- 3D1015D82B27F57400F5C29A /* AddServerView.swift */,
+ 3D91FDC52B28C28900919017 /* SignIn */,
);
path = Views;
sourceTree = "<group>";
@@ -120,6 +126,7 @@
3D1015E02B27FE5700F5C29A /* Models */ = {
isa = PBXGroup;
children = (
+ 3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -187,6 +194,16 @@
path = JelUITests;
sourceTree = "<group>";
};
+ 3D91FDC52B28C28900919017 /* SignIn */ = {
+ isa = PBXGroup;
+ children = (
+ 3D91FDC82B28C62800919017 /* SignInView.swift */,
+ 3D1015D82B27F57400F5C29A /* AddServerView.swift */,
+ 3D91FDCA2B28CA2500919017 /* SignInToServerView.swift */,
+ );
+ path = SignIn;
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -328,7 +345,10 @@
3DF1ED3E2B282836000AD8EA /* JellyfinClientController.swift in Sources */,
3D1015D92B27F57400F5C29A /* AddServerView.swift in Sources */,
3D9063CB2B279A310063DD2A /* JelApp.swift in Sources */,
+ 3D91FDCD2B2907E800919017 /* JellyfinDateFormatter.swift in Sources */,
3D1015DC2B27F5D300F5C29A /* Model.xcdatamodeld in Sources */,
+ 3D91FDC92B28C62800919017 /* SignInView.swift in Sources */,
+ 3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */,
3D1015E42B28000E00F5C29A /* AuthStateController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/Jel/ContentView.swift b/Jel/ContentView.swift
index 2c388be..356615d 100644
--- a/Jel/ContentView.swift
+++ b/Jel/ContentView.swift
@@ -9,11 +9,10 @@ import SwiftUI
struct ContentView: View {
@ObservedObject var authState: AuthStateController
-
var body: some View {
VStack {
if !authState.loggedIn {
- AddServerView(authState: authState)
+ SignInView(authState: authState)
} else {
Text("Logged in")
Button("Log out") {
diff --git a/Jel/Controllers/AuthStateController.swift b/Jel/Controllers/AuthStateController.swift
index 1629556..93dbee3 100644
--- a/Jel/Controllers/AuthStateController.swift
+++ b/Jel/Controllers/AuthStateController.swift
@@ -14,6 +14,8 @@ class AuthStateController: ObservableObject {
private let defaults = UserDefaults.standard
+ static let shared = AuthStateController()
+
init(loggedIn: Bool = false, serverUrl: URL? = nil, authToken: String? = nil) {
self.loggedIn = loggedIn
self.serverUrl = serverUrl
diff --git a/Jel/Controllers/JellyfinClientController.swift b/Jel/Controllers/JellyfinClientController.swift
index 343efe1..b50157e 100644
--- a/Jel/Controllers/JellyfinClientController.swift
+++ b/Jel/Controllers/JellyfinClientController.swift
@@ -9,22 +9,74 @@ import Foundation
import Get
import JellyfinKit
-class JellyfinClientController {
- let api: APIClient
+struct AuthHeaders: Codable {
+ var Client: String
+ var Device: String
+ var DeviceId: String
+ var Version: String
+ var Token: String
+}
+
+enum JellyfinClientError: Error {
+ case badResponseCode
+}
+
+extension AuthHeaders {
+ func format() -> String {
+ return "MediaBrowser Client=\(self.Client), Device=\(self.Device), DeviceId=\(self.DeviceId), Version=\(self.Version), Token=\(self.Token)"
+ }
+}
+
+class JellyfinClientController: ObservableObject {
+ private var api: APIClient
+
+ private var authHeaders: AuthHeaders
+ private var authState: AuthStateController
- init(serverUrl: URL) {
- self.api = APIClient(
- baseURL: serverUrl
- )
+ init(authHeaders: AuthHeaders, serverUrl: URL? = nil, authState: AuthStateController = AuthStateController.shared) {
+ self.authHeaders = authHeaders
+ self.authState = authState
+
+ self.api = APIClient(baseURL: serverUrl)
+ self.setUrl(url: serverUrl)
+ }
+
+ func setToken(token: String) {
+ self.authHeaders.Token = token
+ }
+
+ func setUrl(url: URL?) {
+ if url == nil {
+ return
+ }
+
+ self.api = APIClient(baseURL: url, {
+ $0.sessionConfiguration.httpAdditionalHeaders = ["Authorization": self.authHeaders.format()]
+
+ let decoder = JSONDecoder()
+ decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
+ $0.decoder = decoder
+ })
}
func isJellyfinServer() async -> Bool {
let request = Paths.getPublicUsers
do {
- try await api.send(request)
+ let res = try await api.send(request)
+ if res.statusCode != 200 {
+ throw JellyfinClientError.badResponseCode
+ }
} catch {
return false
}
return true
}
+
+ func signIn(username: String, pw: String) async throws {
+ let request = Paths.authenticateUserByName(AuthenticateUserByName(pw: pw, username: username))
+ let res = try await self.api.send(request)
+ self.authState.loggedIn = true
+ self.authState.authToken = res.value.accessToken
+ self.authState.save()
+ }
}
diff --git a/Jel/JelApp.swift b/Jel/JelApp.swift
index d70e444..6e4490e 100644
--- a/Jel/JelApp.swift
+++ b/Jel/JelApp.swift
@@ -10,14 +10,26 @@ import SwiftUI
@main
struct JelApp: App {
let datamodelController = DatamodelController.shared
- let authStateController = AuthStateController()
+ let authStateController = AuthStateController.shared
+
+ let jellyfinClientController = JellyfinClientController(authHeaders: AuthHeaders(
+ Client: "Jel",
+ Device: UIDevice.current.systemName,
+ DeviceId: UIDevice.current.identifierForVendor!.uuidString,
+ Version: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0",
+ Token: ""))
+
var body: some Scene {
WindowGroup {
ContentView(authState: authStateController)
.environment(\.managedObjectContext,
datamodelController.container.viewContext)
+ .environmentObject(jellyfinClientController)
.task {
authStateController.load()
+ if authStateController.serverUrl != nil {
+ jellyfinClientController.setUrl(url: authStateController.serverUrl!)
+ }
}
}
}
diff --git a/Jel/Models/JellyfinDateFormatter.swift b/Jel/Models/JellyfinDateFormatter.swift
new file mode 100644
index 0000000..74b89d1
--- /dev/null
+++ b/Jel/Models/JellyfinDateFormatter.swift
@@ -0,0 +1,33 @@
+//
+// JellyfinDateFormatter.swift
+// Jel
+//
+// Created by zerocool on 12/12/23.
+//
+
+import Foundation
+
+// from: https://stackoverflow.com/a/46458771
+extension Formatter {
+ static let iso8601withFractionalSeconds: ISO8601DateFormatter = {
+ let formatter = ISO8601DateFormatter()
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+ return formatter
+ }()
+ static let iso8601: ISO8601DateFormatter = {
+ let formatter = ISO8601DateFormatter()
+ formatter.formatOptions = [.withInternetDateTime]
+ return formatter
+ }()
+}
+
+extension JSONDecoder.DateDecodingStrategy {
+ static let iso8601withFractionalSeconds = custom {
+ let container = try $0.singleValueContainer()
+ let string = try container.decode(String.self)
+ if let date = Formatter.iso8601withFractionalSeconds.date(from: string) ?? Formatter.iso8601.date(from: string) {
+ return date
+ }
+ throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
+ }
+}
diff --git a/Jel/Views/AddServerView.swift b/Jel/Views/SignIn/AddServerView.swift
index beab5e7..516b982 100644
--- a/Jel/Views/AddServerView.swift
+++ b/Jel/Views/SignIn/AddServerView.swift
@@ -8,12 +8,14 @@
import SwiftUI
struct AddServerView: View {
+ @EnvironmentObject var jellyfinClient: JellyfinClientController
@ObservedObject var authState: AuthStateController
+ @Binding var serverUrlIsValid: Bool
- @State var serverUrlString: String = ""
+ @State var serverUrlString: String = "http://"
@State var urlHasError: Bool = false
@State var currentErrorMessage: String = ""
- @State var loading: Bool = false
+ @State var isLoading: Bool = false
@FocusState var serverUrlIsFocused: Bool
@@ -22,10 +24,8 @@ struct AddServerView: View {
Text("Connect to a server")
.font(.title)
HStack {
-
TextField(text: $serverUrlString) {
Text("http://jellyfin.example.com")
- .foregroundStyle(.placeholder)
}
.keyboardType(.URL)
.textContentType(.URL)
@@ -33,11 +33,6 @@ struct AddServerView: View {
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($serverUrlIsFocused)
- .onChange(of: serverUrlIsFocused) {
- if serverUrlIsFocused {
- urlHasError = false
- }
- }
.onSubmit {
Task {
await checkServerUrl()
@@ -45,7 +40,7 @@ struct AddServerView: View {
}
- if !loading {
+ if !isLoading {
Button(action: {
Task {
await checkServerUrl()
@@ -55,14 +50,14 @@ struct AddServerView: View {
.labelStyle(.iconOnly)
}
.buttonStyle(.bordered)
- .disabled(urlHasError)
} else {
- ProgressView()
+ ProgressView()
.progressViewStyle(.circular)
- .padding()
+ .padding([.leading, .trailing])
}
}
.padding()
+ .disabled(isLoading)
if urlHasError {
Text(currentErrorMessage)
@@ -73,13 +68,16 @@ struct AddServerView: View {
}
func checkServerUrl() async {
- loading = true
+ isLoading = true
serverUrlIsFocused = false
if isValidUrl(data: serverUrlString) {
let url = URL(string: serverUrlString)!
- if await JellyfinClientController(serverUrl: url).isJellyfinServer() {
+ jellyfinClient.setUrl(url: url)
+ if await jellyfinClient.isJellyfinServer() {
authState.serverUrl = url
+ authState.save()
urlHasError = false
+ serverUrlIsValid = true
} else {
urlHasError = true
currentErrorMessage = "Server not responding"
@@ -90,7 +88,7 @@ struct AddServerView: View {
currentErrorMessage = "Invalid url"
}
- loading = false
+ isLoading = false
}
func isValidUrl(data: String) -> Bool {
@@ -105,5 +103,6 @@ struct AddServerView: View {
}
#Preview {
- AddServerView(authState: AuthStateController())
+ AddServerView(authState: AuthStateController(), serverUrlIsValid: .constant(false))
+
}
diff --git a/Jel/Views/SignIn/SignInToServerView.swift b/Jel/Views/SignIn/SignInToServerView.swift
new file mode 100644
index 0000000..ae8d82d
--- /dev/null
+++ b/Jel/Views/SignIn/SignInToServerView.swift
@@ -0,0 +1,79 @@
+//
+// SignInToServerView.swift
+// Jel
+//
+// Created by zerocool on 12/12/23.
+//
+
+import SwiftUI
+
+struct SignInToServerView: View {
+ @EnvironmentObject var jellyfinClient: JellyfinClientController
+ @ObservedObject var authState: AuthStateController
+
+ @State var username: String = ""
+ @State var password: String = ""
+
+ @State var isLoading: Bool = false
+ @State var hasError: Bool = false
+
+ var body: some View {
+ VStack {
+ Text("Sign in")
+ .font(.title)
+ TextField(text: $username) {
+ Text("Username")
+ }
+ .textContentType(.username)
+
+ SecureField(text: $password) {
+ Text("Password")
+ }
+ .textContentType(.password)
+ .onSubmit {
+ Task {
+ await logInToServer()
+ }
+ }
+
+ if !isLoading {
+ Button {
+ Task {
+ await logInToServer()
+ }
+ } label: {
+ Text("Sign in")
+ }
+ .disabled(username.isEmpty || password.isEmpty)
+ } else {
+ ProgressView()
+ .progressViewStyle(.circular)
+ }
+
+ if hasError {
+ Text("Unable to sign in")
+ .font(.callout)
+ .foregroundStyle(.red)
+ }
+ }
+ .padding()
+ .textFieldStyle(.roundedBorder)
+ .textInputAutocapitalization(.never)
+ .disabled(isLoading)
+ }
+
+ func logInToServer() async {
+ isLoading = true
+ hasError = false
+ do {
+ try await jellyfinClient.signIn(username: username, pw: password)
+ } catch {
+ hasError = true
+ }
+ isLoading = false
+ }
+}
+
+#Preview {
+ SignInToServerView(authState: AuthStateController())
+}
diff --git a/Jel/Views/SignIn/SignInView.swift b/Jel/Views/SignIn/SignInView.swift
new file mode 100644
index 0000000..c06788d
--- /dev/null
+++ b/Jel/Views/SignIn/SignInView.swift
@@ -0,0 +1,42 @@
+//
+// SignInView.swift
+// Jel
+//
+// Created by zerocool on 12/12/23.
+//
+
+import SwiftUI
+
+struct SignInView: View {
+ @EnvironmentObject var jellyfinClient: JellyfinClientController
+ @ObservedObject var authState: AuthStateController
+ @State var serverUrlIsValid: Bool = false
+
+ var body: some View {
+ NavigationStack {
+ AddServerView(authState: authState, serverUrlIsValid: $serverUrlIsValid)
+ .navigationDestination(isPresented: $serverUrlIsValid) {
+ SignInToServerView(authState: authState)
+ }
+ }
+ .onAppear {
+ Task {
+ await checkLoadedServerUrl()
+ }
+ }
+ }
+
+ func checkLoadedServerUrl() async {
+ if authState.serverUrl == nil {
+ return
+ }
+
+ if await jellyfinClient.isJellyfinServer() {
+ serverUrlIsValid = true
+ }
+ }
+}
+
+#Preview {
+ SignInView(authState: AuthStateController())
+}