Skip to content

Commit

Permalink
feat: Authenticated ui and api
Browse files Browse the repository at this point in the history
  • Loading branch information
holzeis committed Jan 23, 2024
1 parent b4aff27 commit 62a36ca
Show file tree
Hide file tree
Showing 15 changed files with 689 additions and 123 deletions.
288 changes: 212 additions & 76 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions webapp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2021"
anyhow = "1"
atty = "0.2.14"
axum = { version = "0.7", features = ["tracing"] }
axum-login = "0.12.0"
axum-server = { version = "0.6", features = ["tls-rustls"] }
bitcoin = "0.29.2"
clap = { version = "4", features = ["derive"] }
Expand All @@ -21,6 +22,7 @@ rust_decimal = { version = "1", features = ["serde-with-float"] }
rust_decimal_macros = "1"
serde = "1.0.147"
serde_json = "1"
sha2 = "0.10"
time = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tower = { version = "0.4", features = ["util"] }
Expand Down
30 changes: 30 additions & 0 deletions webapp/frontend/lib/auth/auth_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:get_10101/common/http_client.dart';
import 'package:get_10101/logger/logger.dart';

class AuthService {
bool isLoggedIn = false;

Future<void> signIn(String password) async {
final response = await HttpClientManager.instance.post(Uri(path: '/api/login'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, dynamic>{'password': password}));

if (response.statusCode != 200) {
throw FlutterError("Failed to login");
}

logger.i("Successfully logged in!");

isLoggedIn = true;
}

Future<void> signOut() async {
await HttpClientManager.instance.get(Uri(path: '/api/logout'));
isLoggedIn = false;
}
}
73 changes: 73 additions & 0 deletions webapp/frontend/lib/auth/login_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:get_10101/auth/auth_service.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/common/text_input_field.dart';
import 'package:get_10101/trade/trade_screen.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';

class LoginScreen extends StatefulWidget {
static const route = "/login";

const LoginScreen({super.key});

@override
State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
String _password = "";

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/10101_logo_icon.png', width: 350, height: 350),
SizedBox(
width: 500,
height: 150,
child: Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey[100],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TextInputField(
value: "",
label: "Password",
obscureText: true,
onSubmitted: (value) => value.isNotEmpty ? signIn(context, value) : (),
onChanged: (value) => setState(() => _password = value),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _password.isEmpty ? null : () => signIn(context, _password),
child: Container(
padding: const EdgeInsets.all(10),
child: const Text(
"Sign in",
style: TextStyle(fontSize: 16),
)))
]),
)),
],
);
}
}

void signIn(BuildContext context, String password) {
final authService = context.read<AuthService>();
authService
.signIn(password)
.then((value) => GoRouter.of(context).go(TradeScreen.route))
.catchError((error) {
final messenger = ScaffoldMessenger.of(context);
showSnackBar(messenger, error?.toString() ?? "Failed to login!");
});
}
16 changes: 10 additions & 6 deletions webapp/frontend/lib/common/http_client.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:http/browser_client.dart';
import 'package:http/http.dart';

class HttpClientManager {
Expand All @@ -7,7 +8,7 @@ class HttpClientManager {
static CustomHttpClient get instance => _httpClient;
}

class CustomHttpClient extends BaseClient {
class CustomHttpClient extends BrowserClient {
// TODO: this should come from the settings

// if this is true, we assume the website is running in dev mode and need to add _host:_port to be able to do http calls
Expand All @@ -18,8 +19,11 @@ class CustomHttpClient extends BaseClient {

final Client _inner;

CustomHttpClient(this._inner, this._dev);
CustomHttpClient(this._inner, this._dev) {
super.withCredentials = true;
}

@override
Future<StreamedResponse> send(BaseRequest request) {
return _inner.send(request);
}
Expand All @@ -28,31 +32,31 @@ class CustomHttpClient extends BaseClient {
Future<Response> delete(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) {
if (_dev && url.host == '') {
url = Uri.parse('http://$_host:$_port${url.toString()}');
url = Uri.parse('https://$_host:$_port${url.toString()}');
}
return _inner.delete(url, headers: headers, body: body, encoding: encoding);
}

@override
Future<Response> put(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) {
if (_dev && url.host == '') {
url = Uri.parse('http://$_host:$_port${url.toString()}');
url = Uri.parse('https://$_host:$_port${url.toString()}');
}
return _inner.put(url, headers: headers, body: body, encoding: encoding);
}

@override
Future<Response> post(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) {
if (_dev && url.host == '') {
url = Uri.parse('http://$_host:$_port${url.toString()}');
url = Uri.parse('https://$_host:$_port${url.toString()}');
}
return _inner.post(url, headers: headers, body: body, encoding: encoding);
}

@override
Future<Response> get(Uri url, {Map<String, String>? headers}) {
if (_dev && url.host == '') {
url = Uri.parse('http://$_host:$_port${url.toString()}');
url = Uri.parse('https://$_host:$_port${url.toString()}');
}
return _inner.get(url, headers: headers);
}
Expand Down
88 changes: 67 additions & 21 deletions webapp/frontend/lib/common/scaffold_with_nav.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:get_10101/auth/auth_service.dart';
import 'package:get_10101/auth/login_screen.dart';
import 'package:get_10101/common/balance.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/common/version_service.dart';
import 'package:get_10101/logger/logger.dart';
import 'package:get_10101/wallet/wallet_service.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -39,6 +45,11 @@ class _ScaffoldWithNestedNavigation extends State<ScaffoldWithNestedNavigation>
String version = "unknown";
Balance balance = Balance.zero();

Timer? _timeout;

// sets the timeout until the user will get automatically logged out after inactivity.
final _inactivityTimout = const Duration(minutes: 5);

void _goBranch(int index) {
widget.navigationShell.goBranch(
index,
Expand All @@ -60,10 +71,25 @@ class _ScaffoldWithNestedNavigation extends State<ScaffoldWithNestedNavigation>
context.read<WalletService>().getBalance().then((b) => setState(() => balance = b));
}

@override
void dispose() {
super.dispose();
_timeout?.cancel();
}

@override
Widget build(BuildContext context) {
final navigationShell = widget.navigationShell;

final authService = context.read<AuthService>();

if (_timeout != null) _timeout!.cancel();
_timeout = Timer(_inactivityTimout, () {
logger.i("Signing out due to inactivity");
authService.signOut();
GoRouter.of(context).go(LoginScreen.route);
});

if (showNavigationDrawer) {
return ScaffoldWithNavigationRail(
body: navigationShell,
Expand Down Expand Up @@ -170,28 +196,48 @@ class ScaffoldWithNavigationRail extends StatelessWidget {
padding: const EdgeInsets.all(25),
child: Row(
children: [
RichText(
text: TextSpan(
text: "Off-chain: ",
style: const TextStyle(fontSize: 16, color: Colors.black),
children: [
TextSpan(
text: balance.offChain.formatted(),
style: const TextStyle(fontWeight: FontWeight.bold)),
const TextSpan(text: " sats"),
]),
Row(
children: [
RichText(
text: TextSpan(
text: "Off-chain: ",
style: const TextStyle(fontSize: 16, color: Colors.black),
children: [
TextSpan(
text: balance.offChain.formatted(),
style: const TextStyle(fontWeight: FontWeight.bold)),
const TextSpan(text: " sats"),
]),
),
const SizedBox(width: 30),
RichText(
text: TextSpan(
text: "On-chain: ",
style: const TextStyle(fontSize: 16, color: Colors.black),
children: [
TextSpan(
text: balance.onChain.formatted(),
style: const TextStyle(fontWeight: FontWeight.bold)),
const TextSpan(text: " sats"),
]),
),
],
),
const SizedBox(width: 30),
RichText(
text: TextSpan(
text: "On-chain: ",
style: const TextStyle(fontSize: 16, color: Colors.black),
children: [
TextSpan(
text: balance.onChain.formatted(),
style: const TextStyle(fontWeight: FontWeight.bold)),
const TextSpan(text: " sats"),
]),
Expanded(
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
ElevatedButton(
onPressed: () {
context
.read<AuthService>()
.signOut()
.then((value) => GoRouter.of(context).go(LoginScreen.route))
.catchError((error) {
final messenger = ScaffoldMessenger.of(context);
showSnackBar(messenger, error);
});
},
child: const Text("Sign out"))
]),
),
],
),
Expand Down
6 changes: 6 additions & 0 deletions webapp/frontend/lib/common/text_input_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ class TextInputField extends StatelessWidget {
this.label = '',
this.hint = '',
this.onChanged,
this.onSubmitted,
required this.value,
this.controller,
this.validator,
this.decoration,
this.style,
this.obscureText = false,
this.onTap});

final TextEditingController? controller;
Expand All @@ -21,8 +23,10 @@ class TextInputField extends StatelessWidget {
final String label;
final String hint;
final Function(String)? onChanged;
final Function(String)? onSubmitted;
final Function()? onTap;
final InputDecoration? decoration;
final bool obscureText;

final String? Function(String?)? validator;

Expand All @@ -33,6 +37,7 @@ class TextInputField extends StatelessWidget {
enabled: enabled,
controller: controller,
initialValue: controller != null ? null : value,
obscureText: obscureText,
decoration: decoration ??
InputDecoration(
border: const OutlineInputBorder(),
Expand All @@ -47,6 +52,7 @@ class TextInputField extends StatelessWidget {
),
onChanged: (value) => {if (onChanged != null) onChanged!(value)},
onTap: onTap,
onFieldSubmitted: (value) => {if (onSubmitted != null) onSubmitted!(value)},
validator: (value) {
if (validator != null) {
return validator!(value);
Expand Down
4 changes: 3 additions & 1 deletion webapp/frontend/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get_10101/auth/auth_service.dart';
import 'package:get_10101/common/version_service.dart';
import 'package:get_10101/logger/logger.dart';
import 'package:get_10101/routes.dart';
Expand All @@ -15,7 +16,8 @@ void main() {

var providers = [
Provider(create: (context) => const VersionService()),
Provider(create: (context) => const WalletService())
Provider(create: (context) => const WalletService()),
Provider(create: (context) => AuthService())
];
runApp(MultiProvider(providers: providers, child: const TenTenOneApp()));
}
Expand Down
Loading

0 comments on commit 62a36ca

Please sign in to comment.