implement auth - almost works

This commit is contained in:
Mikkel Troels Kongsted 2025-03-12 15:56:04 +01:00
parent 28f9bb46e2
commit be00f1c965
19 changed files with 312 additions and 221 deletions

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:mobile/models/product.dart';
import 'package:mobile/results.dart';
class ProductIdException implements Exception {}
@ -82,6 +83,10 @@ class CartController extends ChangeNotifier {
cart.clear();
notifyListeners();
}
Result<Null, String> pay() {
return const Err("Not implemented");
}
}
class CartItem {

View File

@ -1,15 +1,14 @@
import 'package:flutter/material.dart';
import 'package:mobile/models/product.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/mock_server.dart';
import 'package:mobile/server/server.dart';
class ProductController extends ChangeNotifier {
final server = MockServer();
final Server server;
List<Product> products = [];
String query = "";
ProductController() {
ProductController({required this.server}) {
fetchProductsFromServer();
}

View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:mobile/models/user.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart';
class SessionController extends ChangeNotifier {
final Server server;
String? _sessionToken;
SessionController({required this.server});
Future<Result<Null, String>> login(String email, String password) async {
switch (await server.login(email, password)) {
case Success<String>(data: final token):
_sessionToken = token;
notifyListeners();
return const Ok(null);
case Error<String>(message: final message):
notifyListeners();
return Err(message);
}
}
Future<Result<User, Null>> user() async {
final token = _sessionToken;
if (token == null) {
notifyListeners();
return const Err(null);
}
final res = await server.sessionUser(token);
switch (res) {
case Success<User>(data: final user):
return Ok(user);
case Error<User>():
_sessionToken = null;
notifyListeners();
return const Err(null);
}
}
get sessionToken {
return _sessionToken;
}
Future<void> logout() async {
final token = _sessionToken;
if (token != null) {
server.logout(token);
_sessionToken = null;
}
print(_sessionToken);
print("notifying listeners");
notifyListeners();
}
Result<int, String> pay(int userId, int amount) {
return const Err("not implemented");
}
}

View File

@ -1,136 +1,20 @@
import 'package:flutter/material.dart';
import 'package:mobile/results.dart';
import 'package:mobile/server/server.dart';
class UsersControllerOld extends ChangeNotifier {
int nextId = 0;
final List<User> users = [];
class UsersController extends ChangeNotifier {
Server server;
User? _loggedInUser;
UsersController({required this.server});
UsersControllerOld() {
addTestUsers();
}
Result<User, String> getUserById(int id) {
for (var i = 0; i < users.length; i++) {
if (users[i].id == id) {
return Ok(users[i]);
}
Future<Result<Null, String>> register(
String name, String email, String password) async {
final res = await server.register(name, email, password);
switch (res) {
case Success<Null>():
return const Ok(null);
case Error<Null>(message: final message):
return Err(message);
}
return Err("User with id $id doesn't exist");
}
Result<User, String> getUserByMail(String mail) {
for (var i = 0; i < users.length; i++) {
if (users[i].mail == mail) {
return Ok(users[i]);
}
}
return Err("User with mail $mail doesn't exist");
}
Result<User, String> addUser(String name, String mail, String password) {
if (getUserByMail(mail) is Ok) {
return Err("User with mail $mail already exists");
}
final user = User(
id: nextId++,
name: name,
mail: mail,
password: password,
balanceInDkkCents: 0);
users.add(user);
return Ok(user);
}
Result<User, String> login(String mail, String password) {
User? user;
for (var i = 0; i < users.length; i++) {
if (users[i].mail == mail) {
user = users[i];
}
}
if (user == null) {
return Err("User with mail $mail doesn't exist");
}
if (user.password != password) {
return Err("Wrong password for user with mail $mail");
}
_loggedInUser = user;
notifyListeners();
return Ok(user);
}
void logout() {
_loggedInUser = null;
notifyListeners();
}
User? loggedInUser() {
return _loggedInUser;
}
Result<int, String> pay(int userId, int amount) {
final user = getUserById(userId);
if (user is Ok) {
return (user as User).pay(amount);
}
return Err("User with id $userId doesn't exist");
}
void addTestUsers() {
users
..add(User(
id: nextId++,
mail: "test@test.com",
name: "test",
password: "test",
balanceInDkkCents: 10000))
..add(User(
id: nextId++,
mail: "",
name: "",
password: "",
balanceInDkkCents: 100000));
}
void veryBadNotifyAll() {
//
// TODO: THIS SHOULD BE FIXED
// FIXME: DO SOMETHING ELSE PLEASE!!!!!
//
notifyListeners();
}
}
class User {
final int id;
final String mail;
final String name;
final String password;
// balance is in øre
int balanceInDkkCents;
User({
required this.id,
required this.mail,
required this.name,
required this.password,
required this.balanceInDkkCents,
});
void addBalanceFounds(int amount) {
balanceInDkkCents += amount;
}
Result<int, String> pay(int amount) {
if (balanceInDkkCents < amount) {
return Err("User can not afford paying amount $amount");
}
balanceInDkkCents -= amount;
return Ok(balanceInDkkCents);
}
}

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:mobile/controllers/session.dart';
import 'package:mobile/models/user.dart';
import 'package:mobile/pages/dashboard.dart';
import 'package:mobile/pages/log_in_page.dart';
import 'package:mobile/controllers/add_to_cart_state.dart';
import 'package:mobile/controllers/cart.dart';
@ -8,6 +11,7 @@ import 'package:mobile/controllers/paying_state.dart';
import 'package:mobile/controllers/product.dart';
import 'package:mobile/controllers/receipt.dart';
import 'package:mobile/controllers/user.dart';
import 'package:mobile/server/mock_server.dart';
import 'package:provider/provider.dart';
import 'package:mobile/controllers/routing.dart';
@ -20,29 +24,37 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final server = MockServer();
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => RoutingController()),
ChangeNotifierProvider(create: (_) => ProductController()),
ChangeNotifierProvider(
create: (_) => ProductController(server: server)),
ChangeNotifierProvider(create: (_) => CartController()),
ChangeNotifierProvider(create: (_) => ReceiptController()),
ChangeNotifierProvider(create: (_) => PayingStateController()),
ChangeNotifierProvider(create: (_) => AddToCartStateController()),
ChangeNotifierProvider(create: (_) => LocationImageController()),
ChangeNotifierProvider(create: (_) => UsersControllerOld()),
ChangeNotifierProvider(create: (_) => UsersController(server: server)),
ChangeNotifierProvider(
create: (_) => SessionController(server: server)),
],
child: MaterialApp(
title: 'Fresh Plaza',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color.fromARGB(255, 149, 92, 255)),
scaffoldBackgroundColor: const Color(0xFFFAFAFF),
textTheme:
GoogleFonts.merriweatherTextTheme(Theme.of(context).textTheme),
useMaterial3: true,
),
home: const LogInPage(),
),
title: 'Fresh Plaza',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color.fromARGB(255, 149, 92, 255)),
scaffoldBackgroundColor: const Color(0xFFFAFAFF),
textTheme:
GoogleFonts.merriweatherTextTheme(Theme.of(context).textTheme),
useMaterial3: true,
),
home: Consumer<SessionController>(
builder: (_, sessionController, __) {
if (sessionController.sessionToken is String) return Dashboard();
return const LogInPage();
},
)),
);
}
}

View File

@ -0,0 +1,35 @@
import 'package:mobile/results.dart';
class User {
final int id;
final String email;
final String name;
// balance is in øre
int balanceInDkkCents;
User({
required this.id,
required this.email,
required this.name,
required this.balanceInDkkCents,
});
User.fromJson(Map<String, dynamic> json)
: id = json["id"],
email = json["email"],
name = json["name"],
balanceInDkkCents = json["balanceInDkkCents"];
void addBalanceFounds(int amount) {
balanceInDkkCents += amount;
}
Result<int, String> pay(int amount) {
if (balanceInDkkCents < amount) {
return Err("User can not afford paying amount $amount");
}
balanceInDkkCents -= amount;
return Ok(balanceInDkkCents);
}
}

View File

@ -6,7 +6,6 @@ import 'package:mobile/models/product.dart';
import 'package:mobile/pages/finish_shopping_page.dart';
import 'package:mobile/controllers/cart.dart';
import 'package:mobile/controllers/product.dart';
import 'package:mobile/controllers/user.dart';
import 'package:mobile/results.dart';
import 'package:mobile/utils/price.dart';
import 'package:mobile/widgets/primary_button.dart';
@ -144,8 +143,7 @@ class CartItemView extends StatelessWidget {
}
class CartPage extends StatelessWidget {
final User user;
const CartPage({super.key, required this.user});
const CartPage({super.key});
@override
Widget build(BuildContext context) {
@ -316,7 +314,7 @@ class CartPage extends StatelessWidget {
context,
MaterialPageRoute(
builder: (context) =>
FinishShoppingPage(user: user)));
const FinishShoppingPage()));
},
child: const Text("Afslut indkøb")),
),

View File

@ -5,21 +5,16 @@ import 'package:mobile/pages/all_receipts_page.dart';
import 'package:mobile/pages/home_page.dart';
import 'package:mobile/controllers/routing.dart';
import 'package:mobile/controllers/cart.dart';
import 'package:mobile/controllers/user.dart';
import 'package:provider/provider.dart';
class Dashboard extends StatelessWidget {
final User user;
final List<StatelessWidget> pages = [];
Dashboard({super.key, required this.user}) {
Dashboard({super.key}) {
pages.addAll([
HomePage(
user: user,
),
const HomePage(),
const AllProductsPage(),
CartPage(user: user),
const CartPage(),
const AllReceiptsPage(),
]);
}

View File

@ -3,7 +3,6 @@ import 'package:mobile/controllers/routing.dart';
import 'package:mobile/controllers/cart.dart';
import 'package:mobile/controllers/paying_state.dart';
import 'package:mobile/controllers/receipt.dart';
import 'package:mobile/controllers/user.dart';
import 'package:mobile/results.dart';
import 'package:mobile/utils/price.dart';
import 'package:mobile/widgets/primary_button.dart';
@ -11,17 +10,15 @@ import 'package:mobile/widgets/receipt_item.dart';
import 'package:provider/provider.dart';
class FinishShoppingPage extends StatelessWidget {
final User user;
const FinishShoppingPage({super.key, required this.user});
const FinishShoppingPage({super.key});
@override
Widget build(BuildContext context) {
final CartController cartRepo = context.read<CartController>();
final CartController cartController = context.read<CartController>();
final ReceiptController receiptRepo = context.read<ReceiptController>();
final PayingStateController payingStateRepo =
context.watch<PayingStateController>();
final cart = cartRepo.allCartItems();
final cart = cartController.allCartItems();
return Scaffold(
body: SafeArea(
@ -50,7 +47,7 @@ class FinishShoppingPage extends StatelessWidget {
"Total:",
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(formatDkkCents(cartRepo.totalPrice())),
Text(formatDkkCents(cartController.totalPrice())),
],
),
),
@ -60,7 +57,8 @@ class FinishShoppingPage extends StatelessWidget {
onPressed: () async {
payingStateRepo.next();
await Future.delayed(const Duration(seconds: 1));
if (user.pay(cartRepo.totalPrice()) is Err) {
// TODO: implement paying for user
if (cartController.pay() is Err) {
if (context.mounted) {
showDialog<String>(
context: context,
@ -84,7 +82,7 @@ class FinishShoppingPage extends StatelessWidget {
receiptRepo.createReceipt(cart);
payingStateRepo.next();
await Future.delayed(const Duration(seconds: 1));
cartRepo.clearCart();
cartController.clearCart();
payingStateRepo.reset();
if (context.mounted) {
Navigator.pop(context);

View File

@ -1,15 +1,16 @@
import 'package:flutter/material.dart';
import 'package:mobile/controllers/session.dart';
import 'package:mobile/pages/settings_page.dart';
import 'package:mobile/controllers/user.dart';
import 'package:mobile/utils/build_if_session_exists.dart';
import 'package:mobile/utils/price.dart';
import 'package:provider/provider.dart';
class HomePage extends StatelessWidget {
final User user;
const HomePage({super.key, required this.user});
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final sessionController = context.read<SessionController>();
return Column(
children: [
Row(
@ -28,25 +29,29 @@ class HomePage extends StatelessWidget {
),
Card(
child: Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)),
color: Color(0xFFFFFFFF),
),
padding: const EdgeInsets.all(10),
child: Consumer<UsersControllerOld>(
builder: (context, usersRepo, _) => Text(
"Saldo: ${formatDkkCents(user.balanceInDkkCents)}",
style: Theme.of(context).textTheme.headlineSmall)),
),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)),
color: Color(0xFFFFFFFF),
),
padding: const EdgeInsets.all(10),
child: BuildIfSessionExists(
sessionController: sessionController,
placeholder: const CircularProgressIndicator(),
builder: (context, user) => Text(
"Saldo: ${formatDkkCents(user.balanceInDkkCents)}",
style: Theme.of(context).textTheme.headlineSmall))),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Velkommen ${user.name}",
style: Theme.of(context).textTheme.headlineMedium,
),
BuildIfSessionExists(
sessionController: sessionController,
placeholder: const CircularProgressIndicator(),
builder: (context, user) => Text(
"Velkommen ${user.name}",
style: Theme.of(context).textTheme.headlineMedium,
))
],
),
),

View File

@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:mobile/controllers/session.dart';
import 'package:mobile/pages/register_page.dart';
import 'package:mobile/controllers/user.dart';
import 'package:mobile/results.dart';
import 'package:mobile/widgets/error_box.dart';
import 'package:mobile/widgets/primary_button.dart';
import 'package:mobile/widgets/primary_input.dart';
import 'package:provider/provider.dart';
import 'dashboard.dart';
class LogInPage extends StatelessWidget {
const LogInPage({super.key});
@ -61,19 +60,15 @@ class LogInFormState extends State<LogInForm> {
controller: passwordController,
),
PrimaryButton(
onPressed: () {
final usersRepo = context.read<UsersControllerOld>();
final loginResult =
usersRepo.login(mailController.text, passwordController.text);
if (loginResult is Ok) {
setState(() => loginError = false);
Navigator.of(context).popUntil((_) => false);
Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
Dashboard(user: (loginResult as Ok).value)));
} else {
setState(() => loginError = true);
onPressed: () async {
final sessionController = context.read<SessionController>();
final loginResult = await sessionController.login(
mailController.text, passwordController.text);
switch (loginResult) {
case Ok<Null, String>():
setState(() => loginError = false);
case Err<Null, String>():
setState(() => loginError = true);
}
},
child: const Text("Log ind")),

View File

@ -71,9 +71,9 @@ class RegisterFormState extends State<RegisterForm> {
obscure: true),
PrimaryButton(
onPressed: () {
final usersRepo = context.read<UsersControllerOld>();
if (usersRepo.addUser(nameController.text, mailController.text,
passwordController.text) is Ok) {
final sessionsRepo = context.read<UsersController>();
if (sessionsRepo.register(nameController.text,
mailController.text, passwordController.text) is Ok) {
setState(() => registerError = false);
Navigator.of(context).pop();
} else {

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:mobile/pages/log_in_page.dart';
import 'package:mobile/controllers/session.dart';
import 'package:mobile/pages/settings_pages/saldo.dart';
import 'package:mobile/controllers/user.dart';
import 'package:provider/provider.dart';
class _Page {
@ -31,11 +30,9 @@ class SettingsPage extends StatelessWidget {
icon: Icons.door_back_door,
title: "Log ud",
action: (context) {
final users = context.read<UsersControllerOld>();
users.logout();
final sessionsController = context.read<SessionController>();
Navigator.popUntil(context, (_) => false);
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => const LogInPage()));
sessionsController.logout();
}),
];

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:mobile/controllers/user.dart';
import 'package:mobile/controllers/session.dart';
import 'package:mobile/utils/build_if_session_exists.dart';
import 'package:mobile/utils/price.dart';
import 'package:provider/provider.dart';
@ -8,8 +9,7 @@ class SaldoSettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final usersRepo = context.watch<UsersControllerOld>();
final user = usersRepo.loggedInUser()!;
final sessionController = context.watch<SessionController>();
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
@ -25,12 +25,17 @@ class SaldoSettingsPage extends StatelessWidget {
),
],
),
Text("Nuværende saldo: ${formatDkkCents(user.balanceInDkkCents)}",
style: Theme.of(context).textTheme.bodyLarge),
BuildIfSessionExists(
sessionController: sessionController,
placeholder: const CircularProgressIndicator(),
builder: (context, user) => Text(
"Nuværende saldo: ${formatDkkCents(user.balanceInDkkCents)}",
style: Theme.of(context).textTheme.bodyLarge),
),
ElevatedButton.icon(
onPressed: () {
user.addBalanceFounds(10000);
usersRepo.veryBadNotifyAll();
// TODO: implement add balance
throw Exception("not implemented: Adding funds");
},
icon: const Icon(Icons.add),
label: const Text("Tilføj 100,00 kr"),

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:mobile/models/product.dart';
import 'package:mobile/models/user.dart';
import 'package:mobile/server/server.dart';
class BackendServer implements Server {
@ -51,16 +52,61 @@ class BackendServer implements Server {
}
@override
Future<Response<Null>> login(
String name,
Future<Response<String>> login(
String email,
String password,
) async {
final res = await _post(
endpoint: "auth/login",
endpoint: "sessions/login",
body: {"email": email, "password": password},
).then((res) => json.decode(res.body));
if (res["ok"]) {
return Success(data: res["token"]);
} else {
return Error(message: res["message"]);
}
}
@override
Future<Response<Null>> logout(String token) async {
final res = await _post(
endpoint: "sessions/logout",
body: {
"token": token,
},
).then((res) => json.decode(res.body));
if (res["ok"]) {
return Success(data: null);
} else {
return Error(message: res["message"]);
}
}
@override
Future<Response<User>> sessionUser(String token) async {
final res = await http
.get(
Uri.parse("$_apiUrl/sessions/user/$token"),
)
.then((res) => json.decode(res.body));
if (res["ok"]) {
return Error(message: res["message"]);
} else {
return Success(data: User.fromJson(res));
}
}
@override
Future<Response<Null>> payForCart(String token) async {
final res = await _post(
endpoint: "cart/pay",
body: {
"token": token,
},
).then((res) => json.decode(res.body));
if (res["ok"]) {
return Success(data: null);
} else {

View File

@ -1,5 +1,6 @@
import 'package:mobile/models/coordinate.dart';
import 'package:mobile/models/product.dart';
import 'package:mobile/models/user.dart';
import 'package:mobile/server/server.dart';
class MockServer implements Server {
@ -108,11 +109,30 @@ class MockServer implements Server {
}
@override
Future<Response<Null>> login(
String name,
Future<Response<String>> login(
String email,
String password,
) async {
return Success(data: "asdsadasdsad");
}
@override
Future<Response<Null>> logout(String token) async {
return Success(data: null);
}
@override
Future<Response<User>> sessionUser(String token) async {
return Success(
data: User(
id: 0,
email: "test@test.com",
name: "testuser",
balanceInDkkCents: 10000));
}
@override
Future<Response<Null>> payForCart(String token) async {
return Success(data: null);
}
}

View File

@ -1,4 +1,5 @@
import 'package:mobile/models/product.dart';
import 'package:mobile/models/user.dart';
abstract class Server {
Future<Response<List<Product>>> allProducts();
@ -9,11 +10,15 @@ abstract class Server {
String password,
);
Future<Response<Null>> login(
String name,
Future<Response<String>> login(
String email,
String password,
);
Future<Response<Null>> logout(String token);
Future<Response<User>> sessionUser(String token);
Future<Response<Null>> payForCart(String token);
}
sealed class Response<Data> {}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:mobile/controllers/session.dart';
import 'package:mobile/models/user.dart';
import 'package:mobile/results.dart';
class BuildIfSessionExists extends StatelessWidget {
final SessionController sessionController;
final Widget placeholder;
final Widget Function(BuildContext, User) builder;
const BuildIfSessionExists(
{super.key,
required this.sessionController,
required this.placeholder,
required this.builder});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: sessionController.user(),
builder: (context, snapshot) {
final data = snapshot.data;
if (data == null) {
return placeholder;
}
if (data is Ok<User, Null>) {
final user = data.value;
return builder(context, user);
}
return Container();
});
}
}

View File

@ -13,7 +13,7 @@ import 'package:mobile/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);