Skip to content

feat: add login flow & session management #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Coder Desktop/.swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ disabled_rules:
- trailing_comma
type_name:
allowed_symbols: "_"
identifier_name:
allowed_symbols: "_"
19 changes: 19 additions & 0 deletions Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
AA8BC3392D0060A900E1ABAA /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC3382D0060A900E1ABAA /* ViewInspector */; };
AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */; };
AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */; };
AAD720D02D0816B200F6304D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = AAD720CF2D0816B200F6304D /* Alamofire */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -101,6 +102,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
AAD720D02D0816B200F6304D /* Alamofire in Frameworks */,
AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */,
AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */,
);
Expand Down Expand Up @@ -188,6 +190,7 @@
packageProductDependencies = (
AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */,
AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */,
AAD720CF2D0816B200F6304D /* Alamofire */,
);
productName = "Coder Desktop";
productReference = 961678FC2CFF100D00B2B6DF /* Coder Desktop.app */;
Expand Down Expand Up @@ -302,6 +305,7 @@
AA8BC33A2D0060C500E1ABAA /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */,
AA8BC33D2D0061F200E1ABAA /* XCRemoteSwiftPackageReference "fluid-menu-bar-extra" */,
AA8BC4CD2D00A4B700E1ABAA /* XCRemoteSwiftPackageReference "KeychainAccess" */,
AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 961678FD2CFF100D00B2B6DF /* Products */;
Expand Down Expand Up @@ -533,6 +537,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand Down Expand Up @@ -561,6 +566,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand Down Expand Up @@ -778,6 +784,14 @@
kind = branch;
};
};
AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Alamofire/Alamofire";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.10.2;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
Expand All @@ -801,6 +815,11 @@
package = AA8BC4CD2D00A4B700E1ABAA /* XCRemoteSwiftPackageReference "KeychainAccess" */;
productName = KeychainAccess;
};
AAD720CF2D0816B200F6304D /* Alamofire */ = {
isa = XCSwiftPackageProductDependency;
package = AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 961678F42CFF100D00B2B6DF /* Project object */;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"originHash" : "726475d6c2c0355de7a4de72708853eaf53eb295e791efe2cc4b8eb5ce4e9ae8",
"originHash" : "42dc2e0a0e0417a7f4f62b3e875c9559038beef7d2265073dd4fc81f2e11ee13",
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire",
"state" : {
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
"version" : "5.10.2"
}
},
{
"identity" : "fluid-menu-bar-extra",
"kind" : "remoteSourceControl",
Expand Down
47 changes: 47 additions & 0 deletions Coder Desktop/Coder Desktop/About.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import SwiftUI

enum About {
private static var credits: NSAttributedString {
let coder = NSMutableAttributedString(
string: "Coder.com",
attributes: [
.foregroundColor: NSColor.labelColor,
.link: NSURL(string: "https://coder.com")!,
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize),
]
)
let separator = NSAttributedString(
string: " | ",
attributes: [
.foregroundColor: NSColor.labelColor,
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize),
]
)
let source = NSAttributedString(
string: "GitHub",
attributes: [
.foregroundColor: NSColor.labelColor,
.link: NSURL(string: "https://github.com/coder/coder-desktop-macos")!,
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize),
]
)
coder.append(separator)
coder.append(source)
return coder
}

static func open() {
#if compiler(>=5.9) && canImport(AppKit)
if #available(macOS 14, *) {
NSApp.activate()
} else {
NSApp.activate(ignoringOtherApps: true)
}
#else
NSApp.activate(ignoringOtherApps: true)
#endif
NSApp.orderFrontStandardAboutPanel(options: [
.credits: credits,
])
}
}
38 changes: 28 additions & 10 deletions Coder Desktop/Coder Desktop/Coder_DesktopApp.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SwiftUI
import FluidMenuBarExtra
import SwiftUI

@main
struct DesktopApp: App {
Expand All @@ -10,21 +10,39 @@ struct DesktopApp: App {
MenuBarExtra("", isInserted: $hidden) {
EmptyView()
}
Window("Sign In", id: Windows.login.rawValue) {
LoginForm<PreviewClient, PreviewSession>()
}.environmentObject(appDelegate.session)
.environmentObject(appDelegate.client)
.windowResizability(.contentSize)
}
}

class AppDelegate: NSObject, NSApplicationDelegate {
private var menuBarExtra: FluidMenuBarExtra?
// TODO: Replace with real implementations
private var vpn = PreviewVPN()
private var session = PreviewSession()
let vpn: PreviewVPN
let session: PreviewSession
let client: PreviewClient

override init() {
// TODO: Replace with real implementations
client = PreviewClient()
vpn = PreviewVPN()
session = PreviewSession()
}

func applicationDidFinishLaunching(_ notification: Notification) {
self.menuBarExtra = FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
VPNMenu(
vpn: self.vpn,
session: self.session
).frame(width: 256)
func applicationDidFinishLaunching(_: Notification) {
if session.hasSession {
client.initialise(url: session.baseAccessURL!, token: session.sessionToken)
}
menuBarExtra = FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
VPNMenu<PreviewVPN, PreviewSession>().frame(width: 256)
.environmentObject(self.vpn)
.environmentObject(self.session)
}
}

func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
false
}
}
25 changes: 25 additions & 0 deletions Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SwiftUI

class PreviewClient: Client {
required init() {}
func initialise(url _: URL, token _: String?) {}

func user(_: String) async throws -> User {
try await Task.sleep(for: .seconds(1))
return User(
id: UUID(),
username: "admin",
avatar_url: "",
name: "admin",
email: "[email protected]",
created_at: Date.now,
updated_at: Date.now,
last_seen_at: Date.now,
status: "active",
login_type: "none",
theme_preference: "dark",
organization_ids: [],
roles: []
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ class PreviewSession: Session {
baseAccessURL = nil
}

func login(baseAccessURL: URL, sessionToken: String) {
func store(baseAccessURL: URL, sessionToken: String) {
hasSession = true
self.baseAccessURL = baseAccessURL
self.sessionToken = sessionToken
}

func logout() {
func clear() {
hasSession = false
self.baseAccessURL = nil
self.sessionToken = nil
sessionToken = nil
}
}
10 changes: 4 additions & 6 deletions Coder Desktop/Coder Desktop/Preview Content/PreviewVPN.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ class PreviewVPN: Coder_Desktop.VPNService {
@Published var agents: [Coder_Desktop.Agent] = [
Agent(id: UUID(), name: "dogfood2", status: .error, copyableDNS: "asdf.coder", workspaceName: "dogfood2"),
Agent(id: UUID(), name: "testing-a-very-long-name", status: .okay, copyableDNS: "asdf.coder",
workspaceName: "testing-a-very-long-name"
),
workspaceName: "testing-a-very-long-name"),
Agent(id: UUID(), name: "opensrc", status: .warn, copyableDNS: "asdf.coder", workspaceName: "opensrc"),
Agent(id: UUID(), name: "gvisor", status: .off, copyableDNS: "asdf.coder", workspaceName: "gvisor"),
Agent(id: UUID(), name: "example", status: .off, copyableDNS: "asdf.coder", workspaceName: "example"),
Agent(id: UUID(), name: "dogfood2", status: .error, copyableDNS: "asdf.coder", workspaceName: "dogfood2"),
Agent(id: UUID(), name: "testing-a-very-long-name", status: .okay, copyableDNS: "asdf.coder",
workspaceName: "testing-a-very-long-name"
),
workspaceName: "testing-a-very-long-name"),
Agent(id: UUID(), name: "opensrc", status: .warn, copyableDNS: "asdf.coder", workspaceName: "opensrc"),
Agent(id: UUID(), name: "gvisor", status: .off, copyableDNS: "asdf.coder", workspaceName: "gvisor"),
Agent(id: UUID(), name: "example", status: .off, copyableDNS: "asdf.coder", workspaceName: "example"),
Expand All @@ -33,7 +31,7 @@ class PreviewVPN: Coder_Desktop.VPNService {
func start() async {
await setState(.connecting)
do {
try await Task.sleep(nanoseconds: 1000000000)
try await Task.sleep(for: .seconds(1))
} catch {
await setState(.failed(.exampleError))
return
Expand All @@ -49,7 +47,7 @@ class PreviewVPN: Coder_Desktop.VPNService {
guard state == .connected else { return }
await setState(.disconnecting)
do {
try await Task.sleep(nanoseconds: 1000000000) // Simulate network delay
try await Task.sleep(for: .seconds(1))
} catch {
await setState(.failed(.exampleError))
return
Expand Down
65 changes: 65 additions & 0 deletions Coder Desktop/Coder Desktop/SDK/Client.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Alamofire
import Foundation

protocol Client: ObservableObject {
func initialise(url: URL, token: String?)
func user(_ ident: String) async throws -> User
}

class CoderClient: Client {
public var url: URL!
public var token: String?

let decoder: JSONDecoder
let encoder: JSONEncoder

required init() {
encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601withFractionalSeconds
decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
}

func initialise(url: URL, token: String? = nil) {
self.token = token
self.url = url
}

func request<T: Encodable>(
_ path: String,
method: HTTPMethod,
body: T
) async -> DataResponse<Data, AFError> {
let url = self.url.appendingPathComponent(path)
let headers: HTTPHeaders = [Headers.sessionToken: token ?? ""]
return await AF.request(
url,
method: method,
parameters: body,
encoder: JSONParameterEncoder.default,
headers: headers
).serializingData().response
}

func request(
_ path: String,
method: HTTPMethod
) async -> DataResponse<Data, AFError> {
let url = self.url.appendingPathComponent(path)
let headers: HTTPHeaders = [Headers.sessionToken: token ?? ""]
return await AF.request(
url,
method: method,
headers: headers
).serializingData().response
}
}

enum ClientError: Error {
case unexpectedStatusCode
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
case unexpectedStatusCode
case unexpectedStatusCode(Int)

This lets you store the status code

case badResponse
}

enum Headers {
static let sessionToken = "Coder-Session-Token"
}
30 changes: 30 additions & 0 deletions Coder Desktop/Coder Desktop/SDK/Date.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

// Handling for ISO8601 Timestamps with fractional seconds
// Directly from https://stackoverflow.com/questions/46458487/

extension ParseStrategy where Self == Date.ISO8601FormatStyle {
static var iso8601withFractionalSeconds: Self { .init(includingFractionalSeconds: true) }
}

extension JSONDecoder.DateDecodingStrategy {
static let iso8601withOptionalFractionalSeconds = custom {
let string = try $0.singleValueContainer().decode(String.self)
do {
return try .init(string, strategy: .iso8601withFractionalSeconds)
} catch {
return try .init(string, strategy: .iso8601)
}
}
}

extension FormatStyle where Self == Date.ISO8601FormatStyle {
static var iso8601withFractionalSeconds: Self { .init(includingFractionalSeconds: true) }
}

extension JSONEncoder.DateEncodingStrategy {
static let iso8601withFractionalSeconds = custom {
var container = $1.singleValueContainer()
try container.encode($0.formatted(.iso8601withFractionalSeconds))
}
}
Loading