add client ID fetching support, rip out old logins

This commit is contained in:
Henry Hiles 2026-07-18 17:20:18 -04:00
commit fe59f7679c
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
9 changed files with 97 additions and 114 deletions

View file

@ -10,6 +10,7 @@
"msgtype", "msgtype",
"muks", "muks",
"prefs", "prefs",
"unban" "unban",
"unredact"
] ]
} }

2
gomuks

@ -1 +1 @@
Subproject commit 23638a8d2b5ad7ed9f72a0ec39f56cac119c45fb Subproject commit d7506029a62299515356f72061d36ae35e9c9590

View file

@ -20,8 +20,8 @@ import "package:nexus/models/requests/get_event.dart";
import "package:nexus/models/requests/get_related_events.dart"; import "package:nexus/models/requests/get_related_events.dart";
import "package:nexus/models/requests/get_room_state.dart"; import "package:nexus/models/requests/get_room_state.dart";
import "package:nexus/models/requests/join_room.dart"; import "package:nexus/models/requests/join_room.dart";
import "package:nexus/models/requests/login.dart";
import "package:nexus/models/profile.dart"; import "package:nexus/models/profile.dart";
import "package:nexus/models/requests/oauth/register_client.dart";
import "package:nexus/models/requests/paginate.dart"; import "package:nexus/models/requests/paginate.dart";
import "package:nexus/models/requests/redact_event.dart"; import "package:nexus/models/requests/redact_event.dart";
import "package:nexus/models/requests/report.dart"; import "package:nexus/models/requests/report.dart";
@ -275,14 +275,11 @@ class ClientController extends AsyncNotifier<int> {
}); });
} }
Future<String?> login(LoginRequest login) async { Future<String> registerClient(OAuthRegisterClientRequest request) async =>
try { (await _sendCommand(
await _sendCommand("login", login.toJson()); "oauth_register_client",
return null; request.toJson(),
} catch (error) { ))["client_id"];
return error.toString();
}
}
Future<Uri?> discoverHomeserver(Uri homeserver) async { Future<Uri?> discoverHomeserver(Uri homeserver) async {
try { try {

View file

@ -0,0 +1,33 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/shared_prefs.dart";
class ClientIdController extends AsyncNotifier<String> {
static const prefString = "client_id";
@override
Future<String> build() async {
final prefs = await ref.watch(SharedPrefsController.provider.future);
final fromPref = prefs.getString(prefString);
if (fromPref != null) return fromPref;
final fetched = await ref
.watch(ClientController.provider.notifier)
.registerClient(
.new(
homeserverUrl: Uri.https("matrix.federated.nexus"),
clientUri: Uri.https("nexus.federated.nexus"),
),
);
await prefs.setString(prefString, fetched);
return fetched;
}
static final provider =
AsyncNotifierProvider.autoDispose<ClientIdController, String>(
ClientIdController.new,
);
}

View file

@ -2,9 +2,8 @@ import "package:freezed_annotation/freezed_annotation.dart";
part "get_event.freezed.dart"; part "get_event.freezed.dart";
part "get_event.g.dart"; part "get_event.g.dart";
@Freezed() @freezed
abstract class GetEventRequest with _$GetEventRequest { abstract class GetEventRequest with _$GetEventRequest {
const GetEventRequest._();
const factory GetEventRequest({ const factory GetEventRequest({
required String roomId, required String roomId,
required String eventId, required String eventId,

View file

@ -1,15 +0,0 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "login.freezed.dart";
part "login.g.dart";
@freezed
abstract class LoginRequest with _$LoginRequest {
const factory LoginRequest({
required String username,
required String password,
required String homeserverUrl,
}) = _LoginRequest;
factory LoginRequest.fromJson(Map<String, Object?> json) =>
_$LoginRequestFromJson(json);
}

View file

@ -0,0 +1,47 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart";
part "register_client.freezed.dart";
part "register_client.g.dart";
@freezed
abstract class OAuthRegisterClientRequest with _$OAuthRegisterClientRequest {
const factory OAuthRegisterClientRequest({
required Uri homeserverUrl,
ApplicationType? applicationType,
String? clientName,
required Uri clientUri,
Uri? logoUri,
Uri? policyUri,
Uri? tosUri,
IList<GrantType>? grantTypes,
IList<Uri>? redirectUris,
IList<ResponseType>? responseTypes,
AuthMethod? authMethod,
}) = _OAuthRegisterClientRequest;
factory OAuthRegisterClientRequest.fromJson(Map<String, Object?> json) =>
_$OAuthRegisterClientRequestFromJson(json);
}
enum ApplicationType { native, web }
@JsonEnum(fieldRename: .snake)
enum ResponseType { code, idToken }
@JsonEnum(fieldRename: .snake)
enum AuthMethod {
clientSecretPost,
clientSecretBasic,
clientSecretJwt,
privateKeyJwt,
none,
}
@JsonEnum(fieldRename: .snake)
enum GrantType {
authorizationCode,
refreshToken,
clientCredentials,
@JsonValue("urn:ietf:params:oauth:grant-type:device_code")
deviceCode,
}

View file

@ -2,6 +2,8 @@ import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_id_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/appbar.dart";
import "package:nexus/helpers/required_validator_helper.dart"; import "package:nexus/helpers/required_validator_helper.dart";
@ -10,90 +12,9 @@ class LoginPage extends HookConsumerWidget {
const LoginPage({super.key, required this.homeserver}); const LoginPage({super.key, required this.homeserver});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) => Scaffold(
final client = ref.watch(ClientController.provider.notifier); body: ref
.watch(ClientIdController.provider)
final isLoading = useState(false); .betterWhen(data: (value) => Text(value)),
final username = useTextEditingController(); );
final password = useTextEditingController();
final inputError = useState<String?>(null);
final formKey = useRef(GlobalKey<FormState>());
Future<void> tryLogin() async {
isLoading.value = true;
try {
if (formKey.value.currentState?.validate() != true) return;
final error = await client.login(
.new(
username: username.text,
password: password.text,
homeserverUrl: homeserver.origin,
),
);
if (error != null) {
inputError.value = error;
isLoading.value = false;
} else {
if (context.mounted) Navigator.of(context).pop();
}
} finally {
isLoading.value = false;
}
}
return Scaffold(
appBar: Appbar(
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: Navigator.of(context).pop,
),
),
body: AlertDialog(
title: Text("Login to ${homeserver.host}"),
content: Form(
key: formKey.value,
child: Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
TextFormField(
autofocus: true,
textInputAction: .next,
autovalidateMode: .onUserInteraction,
validator: requiredValidator,
decoration: .new(label: Text("Username")),
controller: username,
),
SizedBox(height: 12),
TextFormField(
textInputAction: .done,
decoration: .new(
label: Text("Password"),
errorText: inputError.value,
errorMaxLines: 5,
),
autovalidateMode: .onUserInteraction,
validator: requiredValidator,
controller: password,
obscureText: true,
onFieldSubmitted: (_) => tryLogin(),
// Don't defocus on submit
onEditingComplete: () {},
),
],
),
),
actions: [
TextButton(
onPressed: isLoading.value ? null : tryLogin,
child: Text("Sign In"),
),
],
),
);
}
} }