diff --git a/lib/api/api.dart b/lib/api/api.dart index 6a7bfce..baacb37 100644 --- a/lib/api/api.dart +++ b/lib/api/api.dart @@ -34,6 +34,29 @@ class FeverAPI { return md5.convert(bytes).toString(); } + /// Checks whether the given API URL is a valid Fever API endpoint + static Future isValidAPI(String url) async { + try { + final response = jsonDecode((await http.post(Uri.parse(url))).body); + return response['api_version'] == 3; + } catch(e) { + print(e); + return false; + } + } + + /// Checks whether the given API URL is a valid Fever API endpoint and the given API key is authorized to access it + static Future isAuthenticated(String url, String key) async { + try { + final response = jsonDecode((await http.post(Uri.parse(url), body: { 'api_key': key })).body); + return response['api_version'] == 3 && response['auth'] == 1; + } catch(e) { + print(e); + return false; + } + } + + /// Whether we have an API URL and API key set bool loggedIn() { return apiKey != null && apiUrl != null; } diff --git a/lib/main.dart b/lib/main.dart index 6209aac..4a5ac54 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -35,9 +35,9 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({ super.key, required this.title, required this.api }); final String title; final FeverAPI api; + const MyHomePage({ super.key, required this.title, required this.api }); @override State createState() => _MyHomePageState(); @@ -52,7 +52,7 @@ class _MyHomePageState extends State { ), body: widget.api.loggedIn() ? const Center(child: Text('Meep')) - : const Center(child: LoginPrompt()), + : Center(child: LoginPrompt(api: widget.api)), ); } } diff --git a/lib/pages/login.dart b/lib/pages/login.dart index 4031f45..3b4a1cc 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -1,13 +1,55 @@ +import 'package:feet/api/api.dart'; import 'package:flutter/material.dart'; class LoginPage extends StatefulWidget { - LoginPage({ super.key }); + final FeverAPI api; + const LoginPage({ super.key, required this.api }); @override State createState() => _LoginPageState(); } class _LoginPageState extends State { + bool? _validAPI; + + final _urlController = TextEditingController(); + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + + final _urlFocus = FocusNode(); + + Future checkUrl() async { + if (_urlController.text.isNotEmpty) { + var value = await FeverAPI.isValidAPI(_urlController.text); + setState(() { + _validAPI = value; + }); + + return value; + } + + return false; + } + + @override + void initState() { + super.initState(); + _urlFocus.addListener(() { + if (!_urlFocus.hasFocus) { + checkUrl(); + } + }); + } + + @override + void dispose() { + _urlController.dispose(); + _usernameController.dispose(); + _passwordController.dispose(); + _urlFocus.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -20,18 +62,88 @@ class _LoginPageState extends State { padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Text("Fever compatible API URL"), + children: [ + const Text("Fever compatible API URL"), TextField( + controller: _urlController, + focusNode: _urlFocus, decoration: InputDecoration( - border: OutlineInputBorder(), - contentPadding: EdgeInsets.all(8.0), + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.all(8.0), hintText: 'https://among-us.morbius.sus/api/fever.php', + errorText: _validAPI == false ? 'Invalid or unsupported API' : null, ) - ) + ), ], ), - ) + ), + Container( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Authentication"), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Flexible(child: TextField( + controller: _usernameController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.all(8.0), + hintText: 'Username', + ), + )), + const SizedBox(width: 8), + Flexible(child: TextField( + controller: _passwordController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.all(8.0), + hintText: 'Password', + ), + obscureText: true, + )), + ], + ), + ], + ), + ), + ElevatedButton( + onPressed: () async { + if (_urlController.text.isEmpty) return; + _validAPI ??= await checkUrl(); + if (_validAPI != true || _usernameController.text.isEmpty || _passwordController.text.isEmpty) return; + + var apiKey = FeverAPI.generateApiKey(_usernameController.text, _passwordController.text); + var isAuthenticated = await FeverAPI.isAuthenticated(_urlController.text, apiKey); + + if (!isAuthenticated) { + // ignore: use_build_context_synchronously + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Authentication failed'), + content: const Text('The API URL seems to be valid, but the provided credentials appear to be incorrect.'), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Got it!')), + ], + ); + } + ); + + return; + } + + widget.api.apiUrl = _urlController.text; + widget.api.apiKey = apiKey; + + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + }, + child: const Text('Continue'), + ), ], ), ); diff --git a/lib/widgets/login_prompt.dart b/lib/widgets/login_prompt.dart index 8a12d5c..664069f 100644 --- a/lib/widgets/login_prompt.dart +++ b/lib/widgets/login_prompt.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:feet/pages/login.dart'; +import '../api/api.dart'; class LoginPrompt extends StatelessWidget { - const LoginPrompt({ super.key }); + final FeverAPI api; + const LoginPrompt({ super.key, required this.api }); @override Widget build(BuildContext context) { @@ -11,9 +13,9 @@ class LoginPrompt extends StatelessWidget { const Text('Please log in'), ElevatedButton( onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => LoginPage())); + Navigator.push(context, MaterialPageRoute(builder: (context) => LoginPage(api: api))); }, - child: const Text('Log in') + child: const Text('Log in'), ), ], );