Done login routine
This commit is contained in:
commit
f9b1b25e91
20 changed files with 983 additions and 0 deletions
94
lib/api/api_client.dart
Normal file
94
lib/api/api_client.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
7
lib/api/model/access_token_model.dart
Normal file
7
lib/api/model/access_token_model.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class AccessTokenModel {
|
||||
final String accessToken;
|
||||
|
||||
const AccessTokenModel({
|
||||
required this.accessToken,
|
||||
});
|
||||
}
|
||||
87
lib/main.dart
Normal file
87
lib/main.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
lib/pages/checkup_page.dart
Normal file
52
lib/pages/checkup_page.dart
Normal 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
33
lib/pages/home_page.dart
Normal 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
145
lib/pages/login_page.dart
Normal 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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
21
lib/pages/not_found_page.dart
Normal file
21
lib/pages/not_found_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue