Done login routine

This commit is contained in:
Andrew 2023-04-08 03:51:25 +07:00
commit f9b1b25e91
20 changed files with 983 additions and 0 deletions

94
lib/api/api_client.dart Normal file
View file

@ -0,0 +1,94 @@
import 'dart:async';
import 'dart:convert';
import 'package:tuuli_app/api/model/access_token_model.dart';
import 'package:http/browser_client.dart';
import 'package:http/http.dart';
class ErrorOrData<T> {
final T? data;
final Exception? error;
ErrorOrData(this.data, this.error);
void unfold(
void Function(T data) onData, void Function(Exception error) onError) {
if (data != null) {
onData(data as T);
} else {
onError(error!);
}
}
}
typedef FutureErrorOrData<T> = Future<ErrorOrData<T>>;
class ApiClient {
final BrowserClient _client = BrowserClient();
final Uri baseUrl;
ApiClient(this.baseUrl);
ApiClient.fromString(String baseUrl) : this(Uri.parse(baseUrl));
FutureErrorOrData<AccessTokenModel> login(
String username,
String password,
) async {
AccessTokenModel? data;
Exception? error;
final response = await post('/api/getAccessToken', body: {
'username': username,
'password': password,
}, headers: {
'Content-Type': 'application/json',
});
if (response.statusCode == 200) {
final body = json.decode(await response.stream.bytesToString());
if (body['error'] != null) {
error = Exception(body['error']);
} else if (body['access_token'] == null) {
error = Exception('No access token');
} else {
data = AccessTokenModel(accessToken: body['access_token']);
}
} else {
error = Exception('HTTP ${response.statusCode}');
}
return ErrorOrData(data, error);
}
Future<StreamedResponse> get(
String path, {
Map<String, String>? headers,
}) {
return _request(path, 'GET', headers: headers);
}
Future<StreamedResponse> post(
String path, {
Map<String, String>? headers,
dynamic body,
}) {
return _request(path, 'POST', headers: headers, body: body);
}
Future<StreamedResponse> _request(
String path,
String method, {
Map<String, String>? headers,
dynamic body,
}) async {
final uri = baseUrl.resolve(path);
final request = Request(method, uri);
if (headers != null) {
request.headers.addAll(headers);
}
if (body != null) {
request.body = json.encode(body);
}
return _client.send(request);
}
}

View file

@ -0,0 +1,7 @@
class AccessTokenModel {
final String accessToken;
const AccessTokenModel({
required this.accessToken,
});
}

87
lib/main.dart Normal file
View file

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:tuuli_app/pages/checkup_page.dart';
import 'package:tuuli_app/pages/home_page.dart';
import 'package:tuuli_app/pages/login_page.dart';
import 'package:tuuli_app/pages/not_found_page.dart';
void main() async {
await GetStorage.init();
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
debugShowMaterialGrid: false,
initialRoute: "/login",
onGenerateRoute: _onGenerateRoute,
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.blueGrey,
),
);
}
Route _onGenerateRoute(RouteSettings settings) {
Widget? pageBody;
bool appBarNeeded = true;
switch (settings.name) {
case "/":
appBarNeeded = false;
pageBody = const CheckupPage();
break;
case "/login":
appBarNeeded = false;
pageBody = const LoginPage();
break;
case "/home":
pageBody = const HomePage();
break;
default:
pageBody = const NotFoundPage();
break;
}
return MaterialPageRoute(
builder: (context) => Scaffold(
appBar: appBarNeeded
? AppBar(
title: const Text('GWS Playground'),
actions: [
if (Navigator.of(context).canPop())
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Get.back(canPop: false);
},
),
IconButton(
icon: const Icon(Icons.home),
onPressed: () {
Get.offAllNamed("/");
},
),
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
GetStorage().erase().then((value) {
GetStorage().save();
Get.offAllNamed("/");
});
},
),
],
)
: null,
body: pageBody,
),
);
}
}

View file

@ -0,0 +1,52 @@
import 'package:animated_background/animated_background.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
class CheckupPage extends StatefulWidget {
const CheckupPage({super.key});
@override
State<StatefulWidget> createState() => _CheckupPageState();
}
class _CheckupPageState extends State<CheckupPage>
with TickerProviderStateMixin {
@override
void initState() {
super.initState();
final accessToken = GetStorage().read<String?>("accessToken");
WidgetsBinding.instance.addPostFrameCallback((_) {
if (accessToken == null) {
Get.offAllNamed("/login");
} else {
Get.offAllNamed("/home");
}
});
}
@override
Widget build(BuildContext context) {
return AnimatedBackground(
behaviour: RandomParticleBehaviour(),
vsync: this,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Checking credentials...',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 16),
const SizedBox.square(
dimension: 32,
child: CircularProgressIndicator(),
),
],
),
),
);
}
}

33
lib/pages/home_page.dart Normal file
View file

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<StatefulWidget> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Home Page',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Get.offNamed("/login");
},
child: const Text('Login'),
),
],
),
);
}
}

145
lib/pages/login_page.dart Normal file
View file

@ -0,0 +1,145 @@
import 'dart:async';
import 'package:animated_background/animated_background.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:tuuli_app/api/api_client.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<StatefulWidget> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final apiClient = ApiClient.fromString("http://127.0.0.1:8000");
var submitted = false;
final loginController = TextEditingController();
final passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final formWidth = screenSize.width <= 600 ? screenSize.width : 300.0;
return Stack(
children: [
AnimatedBackground(
behaviour: RandomParticleBehaviour(),
vsync: this,
child: const SizedBox.square(
dimension: 0,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LimitedBox(
maxWidth: formWidth,
child: Container(
color: Colors.black.withAlpha(100),
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: loginController,
enabled: !submitted,
decoration: const InputDecoration(
labelText: 'Login',
hintText: 'Enter your login',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your Login';
}
return null;
},
),
TextFormField(
controller: passwordController,
obscureText: true,
enabled: !submitted,
decoration: const InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
return null;
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: submitted ? null : _submit,
child: const Text('Login'),
),
],
),
),
),
),
],
),
],
);
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
submitted = true;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Trying to login...'),
),
);
final response = await apiClient.login(
loginController.text.trim(),
passwordController.text.trim(),
);
response.unfold((data) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login successful'),
),
);
GetStorage()
.write("accessToken", data.accessToken)
.then((value) => GetStorage().save());
Timer(1.seconds, () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.offAllNamed("/home");
});
});
}, (error) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error.toString()),
),
);
setState(() {
submitted = false;
});
});
}
}

View file

@ -0,0 +1,21 @@
import 'package:animated_background/animated_background.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/scheduler/ticker.dart';
class NotFoundPage extends StatelessWidget {
const NotFoundPage({super.key});
@override
Widget build(BuildContext context) {
return AnimatedBackground(
behaviour: RandomParticleBehaviour(),
vsync: Scaffold.of(context),
child: Center(
child: Text(
'Page not found',
style: Theme.of(context).textTheme.headlineMedium,
),
),
);
}
}