mirror of
https://codeberg.org/secana/Forji.git
synced 2026-06-16 05:13:55 -07:00
feat(auth): add api token login support
This commit is contained in:
parent
9fccd2812f
commit
d60c392489
2 changed files with 121 additions and 50 deletions
|
|
@ -19,6 +19,29 @@ class AuthenticationService {
|
|||
try await storeCredentials(result: result, password: password)
|
||||
}
|
||||
|
||||
func loginWithToken(serverURL: String, token: String, allowSelfSigned: Bool = false) async throws {
|
||||
let normalizedURL = ForgejoClient.normalizeServerURL(serverURL)
|
||||
let tokenClient = ForgejoClient(
|
||||
serverURL: normalizedURL,
|
||||
username: "",
|
||||
token: token,
|
||||
allowSelfSignedCertificates: allowSelfSigned,
|
||||
)
|
||||
let user = try await tokenClient.fetchCurrentUser()
|
||||
let tokenClientWithUsername = ForgejoClient(
|
||||
serverURL: normalizedURL,
|
||||
username: user.login,
|
||||
token: token,
|
||||
allowSelfSignedCertificates: allowSelfSigned,
|
||||
)
|
||||
try await KeychainManager.shared.saveToken(
|
||||
token, for: normalizedURL, username: user.login,
|
||||
)
|
||||
client = tokenClientWithUsername
|
||||
currentUser = user
|
||||
isAuthenticated = true
|
||||
}
|
||||
|
||||
func loginWithOTP(
|
||||
serverURL: String, username: String, password: String, otp: String,
|
||||
allowSelfSigned: Bool = false,
|
||||
|
|
|
|||
|
|
@ -10,11 +10,16 @@ struct InstanceFormView: View {
|
|||
var id: String {
|
||||
switch self {
|
||||
case .add: "add"
|
||||
case let .edit(instance): instance.serverURL + instance.username
|
||||
case .edit(let instance): instance.serverURL + instance.username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AuthMode: String, CaseIterable {
|
||||
case credentials = "Credentials"
|
||||
case token = "API Token"
|
||||
}
|
||||
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Query(sort: \ForgejoInstance.lastUsed, order: .reverse) private var instances: [ForgejoInstance]
|
||||
|
|
@ -29,10 +34,12 @@ struct InstanceFormView: View {
|
|||
@AppStorage("dev_allowSelfSigned") private var devAllowSelfSigned = false
|
||||
#endif
|
||||
|
||||
@State private var authMode: AuthMode = .credentials
|
||||
@State private var name = ""
|
||||
@State private var serverURL = ""
|
||||
@State private var username = ""
|
||||
@State private var password = ""
|
||||
@State private var apiToken = ""
|
||||
@State private var allowSelfSignedCertificates = false
|
||||
@State private var isDefault = false
|
||||
@State private var otpCode = ""
|
||||
|
|
@ -49,7 +56,7 @@ struct InstanceFormView: View {
|
|||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
Section("Forgejo Instance") {
|
||||
TextField("Name (optional)", text: $name)
|
||||
.autocorrectionDisabled()
|
||||
.accessibilityIdentifier("instance-name-field")
|
||||
|
|
@ -60,36 +67,63 @@ struct InstanceFormView: View {
|
|||
.autocorrectionDisabled()
|
||||
.keyboardType(.URL)
|
||||
.accessibilityIdentifier("login-serverURL-field")
|
||||
}
|
||||
|
||||
TextField("Username", text: $username)
|
||||
.textContentType(.username)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled()
|
||||
.accessibilityIdentifier("login-username-field")
|
||||
Section {
|
||||
if authMode == .credentials {
|
||||
TextField("Username", text: $username)
|
||||
.textContentType(.username)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled()
|
||||
.accessibilityIdentifier("login-username-field")
|
||||
|
||||
SecureField("Password", text: $password)
|
||||
.textContentType(.password)
|
||||
.accessibilityIdentifier("login-password-field")
|
||||
SecureField("Password", text: $password)
|
||||
.textContentType(.password)
|
||||
.accessibilityIdentifier("login-password-field")
|
||||
|
||||
if needsOTP {
|
||||
TextField("One-Time Code", text: $otpCode)
|
||||
.keyboardType(.numberPad)
|
||||
.textContentType(.oneTimeCode)
|
||||
.accessibilityIdentifier("login-otp-field")
|
||||
if needsOTP {
|
||||
TextField("One-Time Code", text: $otpCode)
|
||||
.keyboardType(.numberPad)
|
||||
.textContentType(.oneTimeCode)
|
||||
.accessibilityIdentifier("login-otp-field")
|
||||
}
|
||||
} else {
|
||||
SecureField("API Token", text: $apiToken)
|
||||
.textContentType(.password)
|
||||
.autocorrectionDisabled()
|
||||
.accessibilityIdentifier("login-token-field")
|
||||
}
|
||||
} header: {
|
||||
Text("Forgejo Instance")
|
||||
} footer: {
|
||||
if needsOTP {
|
||||
Text("Enter the 6-digit code from your authenticator app.")
|
||||
} else {
|
||||
switch mode {
|
||||
case .add:
|
||||
Text("Enter your Forgejo server URL (e.g., forgejo.example.com)")
|
||||
case .edit:
|
||||
Text("Leave password blank to keep the existing one.")
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Credentials")
|
||||
if isAddMode {
|
||||
Picker("Login Method", selection: $authMode) {
|
||||
ForEach(AuthMode.allCases, id: \.self) { mode in
|
||||
Text(mode.rawValue).tag(mode)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
}
|
||||
.textCase(nil)
|
||||
.padding(.bottom, 4)
|
||||
} footer: {
|
||||
if authMode == .token {
|
||||
Text(
|
||||
"""
|
||||
Generate a token under Settings → Applications with the following scopes:
|
||||
• read/write:user
|
||||
• read/write:repository
|
||||
• read/write:issue
|
||||
• read/write:notification
|
||||
• read:organization
|
||||
"""
|
||||
)
|
||||
} else if needsOTP {
|
||||
Text("Enter the 6-digit code from your authenticator app.")
|
||||
} else if case .edit = mode {
|
||||
Text("Leave password blank to keep the existing one.")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
|
|
@ -122,8 +156,11 @@ struct InstanceFormView: View {
|
|||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(
|
||||
isLoading || serverURL.isEmpty || username.isEmpty
|
||||
|| (isAddMode && password.isEmpty) || (needsOTP && otpCode.isEmpty),
|
||||
isLoading || serverURL.isEmpty
|
||||
|| (authMode == .credentials
|
||||
&& (username.isEmpty || (isAddMode && password.isEmpty)
|
||||
|| (needsOTP && otpCode.isEmpty)))
|
||||
|| (authMode == .token && apiToken.isEmpty),
|
||||
)
|
||||
.listRowBackground(Color.clear)
|
||||
.accessibilityIdentifier("login-button")
|
||||
|
|
@ -157,7 +194,7 @@ struct InstanceFormView: View {
|
|||
if !devPassword.isEmpty { password = devPassword }
|
||||
allowSelfSignedCertificates = devAllowSelfSigned
|
||||
#endif
|
||||
case let .edit(instance):
|
||||
case .edit(let instance):
|
||||
name = instance.name
|
||||
serverURL = instance.serverURL
|
||||
username = instance.username
|
||||
|
|
@ -167,21 +204,30 @@ struct InstanceFormView: View {
|
|||
}
|
||||
|
||||
private func performLogin() async throws {
|
||||
if needsOTP, !otpCode.isEmpty {
|
||||
try await authService.loginWithOTP(
|
||||
switch authMode {
|
||||
case .token:
|
||||
try await authService.loginWithToken(
|
||||
serverURL: serverURL,
|
||||
username: username,
|
||||
password: password,
|
||||
otp: otpCode,
|
||||
allowSelfSigned: allowSelfSignedCertificates,
|
||||
)
|
||||
} else {
|
||||
try await authService.login(
|
||||
serverURL: serverURL,
|
||||
username: username,
|
||||
password: password,
|
||||
token: apiToken,
|
||||
allowSelfSigned: allowSelfSignedCertificates,
|
||||
)
|
||||
case .credentials:
|
||||
if needsOTP, !otpCode.isEmpty {
|
||||
try await authService.loginWithOTP(
|
||||
serverURL: serverURL,
|
||||
username: username,
|
||||
password: password,
|
||||
otp: otpCode,
|
||||
allowSelfSigned: allowSelfSignedCertificates,
|
||||
)
|
||||
} else {
|
||||
try await authService.login(
|
||||
serverURL: serverURL,
|
||||
username: username,
|
||||
password: password,
|
||||
allowSelfSigned: allowSelfSignedCertificates,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,9 +249,11 @@ struct InstanceFormView: View {
|
|||
}
|
||||
|
||||
let normalizedURL = ForgejoClient.normalizeServerURL(serverURL)
|
||||
let resolvedUsername =
|
||||
authMode == .token ? (authService.currentUser?.login ?? username) : username
|
||||
let instance = ForgejoInstance(
|
||||
serverURL: normalizedURL,
|
||||
username: username,
|
||||
username: resolvedUsername,
|
||||
name: name,
|
||||
isDefault: isDefault,
|
||||
allowSelfSignedCertificates: allowSelfSignedCertificates,
|
||||
|
|
@ -218,7 +266,7 @@ struct InstanceFormView: View {
|
|||
}
|
||||
authService.currentInstance = instance
|
||||
|
||||
case let .edit(instance):
|
||||
case .edit(let instance):
|
||||
if !password.isEmpty {
|
||||
try await performLogin()
|
||||
}
|
||||
|
|
@ -257,13 +305,13 @@ struct InstanceFormView: View {
|
|||
}
|
||||
|
||||
#if DEBUG
|
||||
#Preview("Add") {
|
||||
InstanceFormView(authService: .previewDefault, mode: .add)
|
||||
.modelContainer(for: ForgejoInstance.self, inMemory: true)
|
||||
}
|
||||
#Preview("Add") {
|
||||
InstanceFormView(authService: .previewDefault, mode: .add)
|
||||
.modelContainer(for: ForgejoInstance.self, inMemory: true)
|
||||
}
|
||||
|
||||
#Preview("Edit") {
|
||||
InstanceFormView(authService: .previewDefault, mode: .edit(.preview))
|
||||
.modelContainer(for: ForgejoInstance.self, inMemory: true)
|
||||
}
|
||||
#Preview("Edit") {
|
||||
InstanceFormView(authService: .previewDefault, mode: .edit(.preview))
|
||||
.modelContainer(for: ForgejoInstance.self, inMemory: true)
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Reference in a new issue