OAuth Support (#53)

Rips out the old auth method, adds OAuth support. Fixes #51.

Tested Platforms:
- [x] Linux
    - [x] Flatpak
    - [x] Native (Nix)
- [x] Android
- [ ] Windows
- [ ] MacOS

Reviewed-on: Nexus/nexus#53
This commit is contained in:
Henry Hiles 2026-07-20 19:46:18 -04:00 committed by Henry Hiles
commit 7d106ad3a2
86 changed files with 558 additions and 1344 deletions

View file

@ -18,6 +18,7 @@ class ChatPage extends ConsumerWidget {
final showMembersByDefault = constraints.maxWidth > 1000;
final initComplete = ref.watch(InitCompleteController.provider);
final roomId = ref.watch(KeyController.provider(KeyController.roomKey));
ref.read(EmojiController.provider);
return SafeArea(

View file

@ -1,99 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/helpers/required_validator_helper.dart";
class LoginPage extends HookConsumerWidget {
final Uri homeserver;
const LoginPage({super.key, required this.homeserver});
@override
Widget build(BuildContext context, WidgetRef ref) {
final client = ref.watch(ClientController.provider.notifier);
final isLoading = useState(false);
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"),
),
],
),
);
}
}

View file

@ -1,11 +1,14 @@
import "package:app_links/app_links.dart";
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:flutter_svg/flutter_svg.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/auth_url.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_id.dart";
import "package:nexus/helpers/launch_helper.dart";
import "package:nexus/main.dart";
import "package:nexus/models/homeserver.dart";
import "package:nexus/pages/login.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/divider_text.dart";
@ -32,7 +35,7 @@ class SelectServerPage extends HookConsumerWidget {
final newUrl = newHomeserver == null
? null
: await ref
.watch(ClientController.provider.notifier)
.read(ClientController.provider.notifier)
.discoverHomeserver(newHomeserver);
if (context.mounted) {
@ -47,11 +50,42 @@ class SelectServerPage extends HookConsumerWidget {
),
);
} else {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => LoginPage(homeserver: newUrl)),
final codeResponse = await ref.watch(
AuthUrlController.provider(newUrl).future,
);
await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url);
AppLinks().uriLinkStream.listen((encodedUri) async {
final code = encodedUri.queryParameters["code"];
if (code != null) {
await ref
.watch(ClientController.provider.notifier)
.exchangeToken(
.new(
homeserverUrl: newUrl,
codeVerifier: codeResponse.codeVerifier,
redirectUri: .new(
scheme: "nexus.federated.nexus",
path: "/",
),
code: code,
clientId: await ref.watch(
ClientIdController.provider(newUrl).future,
),
),
)
.onError(showError);
}
});
// await Navigator.of(context).push(
// MaterialPageRoute(builder: (_) => LoginPage(homeserver: newUrl)),
// );
}
}
} catch (error, stackTrace) {
showError(error, stackTrace);
} finally {
isLoading.value = false;
}