Skip to content

Commit 6b1e939

Browse files
committedDec 11, 2024
feat: add login flow & session management
1 parent 2c1e8d3 commit 6b1e939

24 files changed

+787
-86
lines changed
 

‎Coder Desktop/.swiftlint.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ disabled_rules:
33
- trailing_comma
44
type_name:
55
allowed_symbols: "_"
6+
identifier_name:
7+
allowed_symbols: "_"

‎Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
AA8BC3392D0060A900E1ABAA /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC3382D0060A900E1ABAA /* ViewInspector */; };
1313
AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */; };
1414
AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */; };
15+
AAD720D02D0816B200F6304D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = AAD720CF2D0816B200F6304D /* Alamofire */; };
1516
/* End PBXBuildFile section */
1617

1718
/* Begin PBXContainerItemProxy section */
@@ -101,6 +102,7 @@
101102
isa = PBXFrameworksBuildPhase;
102103
buildActionMask = 2147483647;
103104
files = (
105+
AAD720D02D0816B200F6304D /* Alamofire in Frameworks */,
104106
AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */,
105107
AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */,
106108
);
@@ -188,6 +190,7 @@
188190
packageProductDependencies = (
189191
AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */,
190192
AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */,
193+
AAD720CF2D0816B200F6304D /* Alamofire */,
191194
);
192195
productName = "Coder Desktop";
193196
productReference = 961678FC2CFF100D00B2B6DF /* Coder Desktop.app */;
@@ -302,6 +305,7 @@
302305
AA8BC33A2D0060C500E1ABAA /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */,
303306
AA8BC33D2D0061F200E1ABAA /* XCRemoteSwiftPackageReference "fluid-menu-bar-extra" */,
304307
AA8BC4CD2D00A4B700E1ABAA /* XCRemoteSwiftPackageReference "KeychainAccess" */,
308+
AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */,
305309
);
306310
preferredProjectObjectVersion = 77;
307311
productRefGroup = 961678FD2CFF100D00B2B6DF /* Products */;
@@ -533,6 +537,7 @@
533537
ENABLE_HARDENED_RUNTIME = YES;
534538
ENABLE_PREVIEWS = YES;
535539
GENERATE_INFOPLIST_FILE = YES;
540+
INFOPLIST_KEY_LSUIElement = YES;
536541
INFOPLIST_KEY_NSHumanReadableCopyright = "";
537542
LD_RUNPATH_SEARCH_PATHS = (
538543
"$(inherited)",
@@ -561,6 +566,7 @@
561566
ENABLE_HARDENED_RUNTIME = YES;
562567
ENABLE_PREVIEWS = YES;
563568
GENERATE_INFOPLIST_FILE = YES;
569+
INFOPLIST_KEY_LSUIElement = YES;
564570
INFOPLIST_KEY_NSHumanReadableCopyright = "";
565571
LD_RUNPATH_SEARCH_PATHS = (
566572
"$(inherited)",
@@ -778,6 +784,14 @@
778784
kind = branch;
779785
};
780786
};
787+
AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */ = {
788+
isa = XCRemoteSwiftPackageReference;
789+
repositoryURL = "https://github.com/Alamofire/Alamofire";
790+
requirement = {
791+
kind = upToNextMajorVersion;
792+
minimumVersion = 5.10.2;
793+
};
794+
};
781795
/* End XCRemoteSwiftPackageReference section */
782796

783797
/* Begin XCSwiftPackageProductDependency section */
@@ -801,6 +815,11 @@
801815
package = AA8BC4CD2D00A4B700E1ABAA /* XCRemoteSwiftPackageReference "KeychainAccess" */;
802816
productName = KeychainAccess;
803817
};
818+
AAD720CF2D0816B200F6304D /* Alamofire */ = {
819+
isa = XCSwiftPackageProductDependency;
820+
package = AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */;
821+
productName = Alamofire;
822+
};
804823
/* End XCSwiftPackageProductDependency section */
805824
};
806825
rootObject = 961678F42CFF100D00B2B6DF /* Project object */;

‎Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
{
2-
"originHash" : "726475d6c2c0355de7a4de72708853eaf53eb295e791efe2cc4b8eb5ce4e9ae8",
2+
"originHash" : "42dc2e0a0e0417a7f4f62b3e875c9559038beef7d2265073dd4fc81f2e11ee13",
33
"pins" : [
4+
{
5+
"identity" : "alamofire",
6+
"kind" : "remoteSourceControl",
7+
"location" : "https://github.com/Alamofire/Alamofire",
8+
"state" : {
9+
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
10+
"version" : "5.10.2"
11+
}
12+
},
413
{
514
"identity" : "fluid-menu-bar-extra",
615
"kind" : "remoteSourceControl",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import SwiftUI
2+
3+
enum About {
4+
private static var credits: NSAttributedString {
5+
let coder = NSMutableAttributedString(
6+
string: "Coder.com",
7+
attributes: [
8+
.foregroundColor: NSColor.labelColor,
9+
.link: NSURL(string: "https://coder.com")!,
10+
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize),
11+
]
12+
)
13+
let separator = NSAttributedString(
14+
string: " | ",
15+
attributes: [
16+
.foregroundColor: NSColor.labelColor,
17+
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize),
18+
]
19+
)
20+
let source = NSAttributedString(
21+
string: "GitHub",
22+
attributes: [
23+
.foregroundColor: NSColor.labelColor,
24+
.link: NSURL(string: "https://github.com/coder/coder-desktop-macos")!,
25+
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize),
26+
]
27+
)
28+
coder.append(separator)
29+
coder.append(source)
30+
return coder
31+
}
32+
33+
static func open() {
34+
#if compiler(>=5.9) && canImport(AppKit)
35+
if #available(macOS 14, *) {
36+
NSApp.activate()
37+
} else {
38+
NSApp.activate(ignoringOtherApps: true)
39+
}
40+
#else
41+
NSApp.activate(ignoringOtherApps: true)
42+
#endif
43+
NSApp.orderFrontStandardAboutPanel(options: [
44+
.credits: credits,
45+
])
46+
}
47+
}
Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import SwiftUI
21
import FluidMenuBarExtra
2+
import SwiftUI
33

44
@main
55
struct DesktopApp: App {
@@ -10,21 +10,39 @@ struct DesktopApp: App {
1010
MenuBarExtra("", isInserted: $hidden) {
1111
EmptyView()
1212
}
13+
Window("Sign In", id: Windows.login.rawValue) {
14+
LoginForm<PreviewClient, PreviewSession>()
15+
}.environmentObject(appDelegate.session)
16+
.environmentObject(appDelegate.client)
17+
.windowResizability(.contentSize)
1318
}
1419
}
1520

1621
class AppDelegate: NSObject, NSApplicationDelegate {
1722
private var menuBarExtra: FluidMenuBarExtra?
18-
// TODO: Replace with real implementations
19-
private var vpn = PreviewVPN()
20-
private var session = PreviewSession()
23+
let vpn: PreviewVPN
24+
let session: PreviewSession
25+
let client: PreviewClient
26+
27+
override init() {
28+
// TODO: Replace with real implementations
29+
client = PreviewClient()
30+
vpn = PreviewVPN()
31+
session = PreviewSession()
32+
}
2133

22-
func applicationDidFinishLaunching(_ notification: Notification) {
23-
self.menuBarExtra = FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
24-
VPNMenu(
25-
vpn: self.vpn,
26-
session: self.session
27-
).frame(width: 256)
34+
func applicationDidFinishLaunching(_: Notification) {
35+
if session.hasSession {
36+
client.initialise(url: session.baseAccessURL!, token: session.sessionToken)
2837
}
38+
menuBarExtra = FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
39+
VPNMenu<PreviewVPN, PreviewSession>().frame(width: 256)
40+
.environmentObject(self.vpn)
41+
.environmentObject(self.session)
42+
}
43+
}
44+
45+
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
46+
false
2947
}
3048
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import SwiftUI
2+
3+
class PreviewClient: Client {
4+
required init() {}
5+
func initialise(url _: URL, token _: String?) {}
6+
7+
func user(_: String) async throws -> User {
8+
try await Task.sleep(for: .seconds(1))
9+
return User(
10+
id: UUID(),
11+
username: "admin",
12+
avatar_url: "",
13+
name: "admin",
14+
email: "admin@coder.com",
15+
created_at: Date.now,
16+
updated_at: Date.now,
17+
last_seen_at: Date.now,
18+
status: "active",
19+
login_type: "none",
20+
theme_preference: "dark",
21+
organization_ids: [],
22+
roles: []
23+
)
24+
}
25+
}

‎Coder Desktop/Coder Desktop/Preview Content/PreviewSession.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@ class PreviewSession: Session {
1111
baseAccessURL = nil
1212
}
1313

14-
func login(baseAccessURL: URL, sessionToken: String) {
14+
func store(baseAccessURL: URL, sessionToken: String) {
1515
hasSession = true
1616
self.baseAccessURL = baseAccessURL
1717
self.sessionToken = sessionToken
1818
}
1919

20-
func logout() {
20+
func clear() {
2121
hasSession = false
22-
self.baseAccessURL = nil
23-
self.sessionToken = nil
22+
sessionToken = nil
2423
}
2524
}

‎Coder Desktop/Coder Desktop/Preview Content/PreviewVPN.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@ class PreviewVPN: Coder_Desktop.VPNService {
55
@Published var agents: [Coder_Desktop.Agent] = [
66
Agent(id: UUID(), name: "dogfood2", status: .error, copyableDNS: "asdf.coder", workspaceName: "dogfood2"),
77
Agent(id: UUID(), name: "testing-a-very-long-name", status: .okay, copyableDNS: "asdf.coder",
8-
workspaceName: "testing-a-very-long-name"
9-
),
8+
workspaceName: "testing-a-very-long-name"),
109
Agent(id: UUID(), name: "opensrc", status: .warn, copyableDNS: "asdf.coder", workspaceName: "opensrc"),
1110
Agent(id: UUID(), name: "gvisor", status: .off, copyableDNS: "asdf.coder", workspaceName: "gvisor"),
1211
Agent(id: UUID(), name: "example", status: .off, copyableDNS: "asdf.coder", workspaceName: "example"),
1312
Agent(id: UUID(), name: "dogfood2", status: .error, copyableDNS: "asdf.coder", workspaceName: "dogfood2"),
1413
Agent(id: UUID(), name: "testing-a-very-long-name", status: .okay, copyableDNS: "asdf.coder",
15-
workspaceName: "testing-a-very-long-name"
16-
),
14+
workspaceName: "testing-a-very-long-name"),
1715
Agent(id: UUID(), name: "opensrc", status: .warn, copyableDNS: "asdf.coder", workspaceName: "opensrc"),
1816
Agent(id: UUID(), name: "gvisor", status: .off, copyableDNS: "asdf.coder", workspaceName: "gvisor"),
1917
Agent(id: UUID(), name: "example", status: .off, copyableDNS: "asdf.coder", workspaceName: "example"),
@@ -33,7 +31,7 @@ class PreviewVPN: Coder_Desktop.VPNService {
3331
func start() async {
3432
await setState(.connecting)
3533
do {
36-
try await Task.sleep(nanoseconds: 1000000000)
34+
try await Task.sleep(for: .seconds(1))
3735
} catch {
3836
await setState(.failed(.exampleError))
3937
return
@@ -49,7 +47,7 @@ class PreviewVPN: Coder_Desktop.VPNService {
4947
guard state == .connected else { return }
5048
await setState(.disconnecting)
5149
do {
52-
try await Task.sleep(nanoseconds: 1000000000) // Simulate network delay
50+
try await Task.sleep(for: .seconds(1))
5351
} catch {
5452
await setState(.failed(.exampleError))
5553
return
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Alamofire
2+
import Foundation
3+
4+
protocol Client: ObservableObject {
5+
func initialise(url: URL, token: String?)
6+
func user(_ ident: String) async throws -> User
7+
}
8+
9+
class CoderClient: Client {
10+
public var url: URL!
11+
public var token: String?
12+
13+
let decoder: JSONDecoder
14+
let encoder: JSONEncoder
15+
16+
required init() {
17+
encoder = JSONEncoder()
18+
encoder.dateEncodingStrategy = .iso8601withFractionalSeconds
19+
decoder = JSONDecoder()
20+
decoder.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
21+
}
22+
23+
func initialise(url: URL, token: String? = nil) {
24+
self.token = token
25+
self.url = url
26+
}
27+
28+
func request<T: Encodable>(
29+
_ path: String,
30+
method: HTTPMethod,
31+
body: T
32+
) async -> DataResponse<Data, AFError> {
33+
let url = self.url.appendingPathComponent(path)
34+
let headers: HTTPHeaders = [Headers.sessionToken: token ?? ""]
35+
return await AF.request(
36+
url,
37+
method: method,
38+
parameters: body,
39+
encoder: JSONParameterEncoder.default,
40+
headers: headers
41+
).serializingData().response
42+
}
43+
44+
func request(
45+
_ path: String,
46+
method: HTTPMethod
47+
) async -> DataResponse<Data, AFError> {
48+
let url = self.url.appendingPathComponent(path)
49+
let headers: HTTPHeaders = [Headers.sessionToken: token ?? ""]
50+
return await AF.request(
51+
url,
52+
method: method,
53+
headers: headers
54+
).serializingData().response
55+
}
56+
}
57+
58+
enum ClientError: Error {
59+
case unexpectedStatusCode
60+
case badResponse
61+
}
62+
63+
enum Headers {
64+
static let sessionToken = "Coder-Session-Token"
65+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
3+
// Handling for ISO8601 Timestamps with fractional seconds
4+
// Directly from https://stackoverflow.com/questions/46458487/
5+
6+
extension ParseStrategy where Self == Date.ISO8601FormatStyle {
7+
static var iso8601withFractionalSeconds: Self { .init(includingFractionalSeconds: true) }
8+
}
9+
10+
extension JSONDecoder.DateDecodingStrategy {
11+
static let iso8601withOptionalFractionalSeconds = custom {
12+
let string = try $0.singleValueContainer().decode(String.self)
13+
do {
14+
return try .init(string, strategy: .iso8601withFractionalSeconds)
15+
} catch {
16+
return try .init(string, strategy: .iso8601)
17+
}
18+
}
19+
}
20+
21+
extension FormatStyle where Self == Date.ISO8601FormatStyle {
22+
static var iso8601withFractionalSeconds: Self { .init(includingFractionalSeconds: true) }
23+
}
24+
25+
extension JSONEncoder.DateEncodingStrategy {
26+
static let iso8601withFractionalSeconds = custom {
27+
var container = $1.singleValueContainer()
28+
try container.encode($0.formatted(.iso8601withFractionalSeconds))
29+
}
30+
}

0 commit comments

Comments
 (0)