Skip to content

Commit

Permalink
Merge pull request #1876 from get10101/feat/add-authentication-to-webapp
Browse files Browse the repository at this point in the history
chore: Add authentication to web app
  • Loading branch information
holzeis authored Jan 23, 2024
2 parents ee6609a + 649b2b7 commit 2f514da
Show file tree
Hide file tree
Showing 24 changed files with 934 additions and 208 deletions.
344 changes: 268 additions & 76 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ web:
web-release:
cd webapp/frontend && flutter build web --release

run-web:
cd webapp/frontend && flutter run -d chrome --web-browser-flag "--disable-web-security"

# Build Rust library for iOS (debug mode)
ios:
cd mobile/native && CARGO_TARGET_DIR=../../target/ios_debug cargo lipo
Expand Down
3 changes: 3 additions & 0 deletions webapp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ 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"] }
commons = { path = "../crates/commons" }
Expand All @@ -20,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
4 changes: 2 additions & 2 deletions webapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ flutter build web
## Run the rust app

```bash
cargo run
cargo run -- --cert-dir certs --data-dir ../data
```

The webinterface will be reachable under `http://localhost:3001`
The webinterface will be reachable under `https://localhost:3001`

Note: if you can't see anything, you probably forgot to run `flutter build web` before
34 changes: 34 additions & 0 deletions webapp/certs/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF7zCCA9egAwIBAgIUOzK208NVNruWNbI/0ev22Ln01dswDQYJKoZIhvcNAQEL
BQAwgYYxCzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcM
CENpdHlOYW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFu
eVNlY3Rpb25OYW1lMR0wGwYDVQQDDBRDb21tb25OYW1lT3JIb3N0bmFtZTAeFw0y
NDAxMjIxMzA1MjBaFw0zNDAxMTkxMzA1MjBaMIGGMQswCQYDVQQGEwJYWDESMBAG
A1UECAwJU3RhdGVOYW1lMREwDwYDVQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29t
cGFueU5hbWUxGzAZBgNVBAsMEkNvbXBhbnlTZWN0aW9uTmFtZTEdMBsGA1UEAwwU
Q29tbW9uTmFtZU9ySG9zdG5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQDEQeFii40f/4BvccPIaEjW/HuC5V5hK7iEdQKaIg/Fo0R8040R6dIjq6yB
hzqQu55GzctjrsfvdyvmT4N4S0bK/gdeG4PPlkh/t5jWZmV7eI9XoedjT6UnrQYb
hh2sAuSoK4Snc8H40SlexLngRYHLK7Wu0xbip94NOGU9x3s8mvNB7v04aVeEPER/
6q0GPjPxNHQi3pYD9v1PdRM5sjYch6zOBUsVAaam7jj2UgF38ERxXqd2JNKDZovx
wyYmmWxfjxx3LmH94d69XkwBR/QUZk9xKIAGaKyceA3G3Vu22bjQd6zR2Eefy337
ZpvqHlqt82Zhj139TeR/cfJt6haaHZoedMI4QcaRacIFDZUq6snfftwP0QLQ2y12
MN5mPI8bAFwIk2LyZdKXDSWxan61RT+twCJJ4S8KFryFU6MC6OScf+Eh9D5yhe12
hdFftcbVcuwp5vv6FE2GgsMQQe3rC2C9w+/ag9rcIXn1tdnfBQt8YxEzFkFPtoOP
2q1LURSwy5LYwN6y/vv5B7vi6NWDONZuz+1ZMBTfSCMrI9NfQsHnP1dMmxbVa/Dx
JB4opHb5mXWBwrob77X2UaB+gHquWGf4spLa7TJ5Gcd+p6bcFtidWg9aFkK8VV11
X9GUQBD/wT8R0+gE1rbnBHzAM28x09B+oO70KskRM5ITXOTvjwIDAQABo1MwUTAd
BgNVHQ4EFgQURPD83wu33omr4giNGe17BaztDPIwHwYDVR0jBBgwFoAURPD83wu3
3omr4giNGe17BaztDPIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
AgEAT/c/AJ+i86wlrin+nPoyAS9TpkN2YJ+6knlNApg17KfYZ0mmEHEI8pve+8jJ
wiEXtT0fkCfy6CtuNuwGF72L68bJU5DsaGCLviP53vmCRkm9KbQ/BwqyiJQQ9aiU
V/Ue1wvGhJy262XkPC+xvjbT6FISaq093OpIOBZYPBERSTCDdvygFlBmjB74CFKq
+RvBgZbOYmkYyRwGeiI54wLw1I1NGP+yM10vEgrHvsPV/GtM9kOvSnS7GAnq6+oH
ji64KZzDEqrMdKRbfapGGTIK6OOax1RAnvNjAWteLwlRBOvf4t8E/rylpdPFprBz
mfnaok/8LXWaM8ax1CA1mtqET3cz2jzT2rG9+uI+NU4xmnykWQisNRZEe+3vT+O+
dRRXdyRoL2sSL8PLQeMA8NVdglpiQILB4KnZr/8m0qcne/IAy6yqQT9YGaCn/Kdb
Y5gI1jvnUOqTJU0oEft6hynC+k/f7EWqy6fvFNPFKgnLzl3ahOQzIfoxUUyCzErA
5X61TJj1PEDyLNVuNqb1O3sfDCPKLGDvySO5jPc6ZOlRJp1ajSFWTIUjn5Keizd1
jrDNgUbW5KhEVjcx9mrzFMWYZgimGRIMFHfv96+WleWDdDeAdFAPRvnvcUX++VyG
NPb3/LNEu9DMBb6Hhzhousibzcqpuykvy9Xi4pRBjXjwXf4=
-----END CERTIFICATE-----
52 changes: 52 additions & 0 deletions webapp/certs/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
SNUpDciiMSMdODrlzWzX9El2ftWq
-----END PRIVATE KEY-----
34 changes: 34 additions & 0 deletions webapp/frontend/lib/auth/auth_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 {
Future<bool> isLoggedIn() async {
try {
final response = await HttpClientManager.instance.get(Uri(path: '/api/version'));
return response.statusCode == 200;
} catch (error) {
return 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!");
}

Future<void> signOut() async {
await HttpClientManager.instance.get(Uri(path: '/api/logout'));
}
}
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!");
});
}
11 changes: 8 additions & 3 deletions webapp/frontend/lib/common/http_client.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/browser_client.dart';
import 'package:http/http.dart';

class HttpClientManager {
static final CustomHttpClient _httpClient = CustomHttpClient(Client(), true);
static final CustomHttpClient _httpClient = CustomHttpClient(Client(), kDebugMode);

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 +20,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 Down
99 changes: 72 additions & 27 deletions webapp/frontend/lib/common/scaffold_with_nav.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
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/trade/orderbook_service.dart';
Expand Down Expand Up @@ -42,6 +47,11 @@ class _ScaffoldWithNestedNavigation extends State<ScaffoldWithNestedNavigation>
Balance balance = Balance.zero();
BestQuote? bestQuote;

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 @@ -66,10 +76,25 @@ class _ScaffoldWithNestedNavigation extends State<ScaffoldWithNestedNavigation>
}));
}

@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 @@ -179,33 +204,53 @@ class ScaffoldWithNavigationRail extends StatelessWidget {
padding: const EdgeInsets.all(25),
child: Row(
children: [
TopBarItem(label: 'Latest Bid: ', value: [
TextSpan(
text: bestQuote?.bid?.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
)
]),
const SizedBox(width: 30),
TopBarItem(label: 'Latest Ask: ', value: [
TextSpan(
text: bestQuote?.ask?.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
)
]),
const SizedBox(width: 30),
TopBarItem(label: 'Off-chain: ', value: [
TextSpan(
text: balance.offChain.formatted(),
style: const TextStyle(fontWeight: FontWeight.bold)),
const TextSpan(text: " sats"),
]),
const SizedBox(width: 30),
TopBarItem(label: 'On-chain: ', value: [
TextSpan(
text: balance.onChain.formatted(),
style: const TextStyle(fontWeight: FontWeight.bold)),
const TextSpan(text: " sats"),
]),
Row(
children: [
TopBarItem(label: 'Latest Bid: ', value: [
TextSpan(
text: bestQuote?.bid?.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
)
]),
const SizedBox(width: 30),
TopBarItem(label: 'Latest Ask: ', value: [
TextSpan(
text: bestQuote?.ask?.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
)
]),
const SizedBox(width: 30),
TopBarItem(label: 'Off-chain: ', value: [
TextSpan(
text: balance.offChain.formatted(),
style: const TextStyle(fontWeight: FontWeight.bold)),
const TextSpan(text: " sats"),
]),
const SizedBox(width: 30),
TopBarItem(label: 'On-chain: ', value: [
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
Loading

0 comments on commit 2f514da

Please sign in to comment.