Login page checks URL and credentials

This commit is contained in:
Jan 2022-12-29 21:05:14 +01:00
parent 3f8599a3e9
commit e3c15d687b
Signed by: Lea
GPG key ID: 1BAFFE8347019C42
4 changed files with 149 additions and 12 deletions

View file

@ -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<bool> 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<bool> 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;
}

View file

@ -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<MyHomePage> createState() => _MyHomePageState();
@ -52,7 +52,7 @@ class _MyHomePageState extends State<MyHomePage> {
),
body: widget.api.loggedIn()
? const Center(child: Text('Meep'))
: const Center(child: LoginPrompt()),
: Center(child: LoginPrompt(api: widget.api)),
);
}
}

View file

@ -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<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
bool? _validAPI;
final _urlController = TextEditingController();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _urlFocus = FocusNode();
Future<bool> 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<LoginPage> {
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'),
),
],
),
);

View file

@ -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'),
),
],
);