forked from ReiMerc/skantravels
Compare commits
18 Commits
4539d48c29
...
46f9535b4a
Author | SHA1 | Date | |
---|---|---|---|
46f9535b4a | |||
acb15df909 | |||
7c2fccb79e | |||
46bb1ce74c | |||
206483de09 | |||
102e2d6ca2 | |||
c4fc11c169 | |||
|
6ee61650d1 | ||
8e42e54c98 | |||
eef47f6ee4 | |||
debbddd05b | |||
c9769fcada | |||
|
a082441826 | ||
1c6fc39b14 | |||
3bd2ba5cbc | |||
d2bfcc4692 | |||
|
f1327571e6 | ||
bb44d2bc56 |
@ -1,6 +0,0 @@
|
||||
namespace API.Application.Users.Commands
|
||||
{
|
||||
public class UpdateUserPassword
|
||||
{
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
| | Scrum Master | Product Owner | Kundechef |
|
||||
| ---------- | ------------ | ------------- | --------- |
|
||||
| **Uge 32** | Alexander | Reimar | Phillip |
|
||||
| **Uge 33** | Phillip | Alexander | Reimar |
|
||||
| **Uge 34** | Reimar | Phillip | Alexander |
|
||||
| **Uge 35** | Alexander | Reimar | Phillip |
|
||||
| **Uge 36** | Phillip | Alexander | Reimar |
|
||||
| **Uge 37** | Reimar | Phillip | Alexander |
|
||||
| **Uge 32** | Alexander | Reimar | Philip |
|
||||
| **Uge 33** | Philip | Alexander | Reimar |
|
||||
| **Uge 34** | Reimar | Philip | Alexander |
|
||||
| **Uge 35** | Alexander | Reimar | Philip |
|
||||
| **Uge 36** | Philip | Alexander | Reimar |
|
||||
| **Uge 37** | Reimar | Philip | Alexander |
|
||||
|
||||
# Daglig Logbog Uge 32
|
||||
|
||||
@ -66,17 +66,59 @@ KC - Kundechef
|
||||
|
||||
## Torsdag
|
||||
|
||||
**PO(Alexander):**
|
||||
**PO(Alexander):** Manglende pga. sygdom
|
||||
|
||||
**KC(Reimar):**
|
||||
**KC(Reimar):** Implementeret JWT verifikation i rust-backenden
|
||||
|
||||
**SM(Philip):** Var med til et møde sammen med reimer da programmeringsgruppen skulle bruge hjælp. Tilføjede selv authorization på swagger appikationen
|
||||
|
||||
## Fredag
|
||||
|
||||
**PO(Alexander):**
|
||||
**PO(Alexander):** Manglende pga. kommunemøde
|
||||
|
||||
**KC(Reimar):**
|
||||
**KC(Reimar):** Lavet model og routes til oprettelse/sletning af favoritter i rust
|
||||
|
||||
**SM(Philip):**
|
||||
**SM(Philip):**
|
||||
|
||||
# Uge 34
|
||||
|
||||
## Mandag
|
||||
|
||||
**KC(Alexander):** Mødte op, lavede hurlumhaj, lærte om Flutter. Geninstallerede min laptop fordi Windows var træls. Fik sat alle værktøjerne op på Linux Mint
|
||||
|
||||
**SM(Reimar):** Kørte gennem flutter-tutorial
|
||||
|
||||
**PO(Philip):** Flutter tutorial
|
||||
|
||||
## Tirsdag
|
||||
|
||||
**KC(Alexander):** Ramt af sygdom, fucking tirsdage
|
||||
|
||||
**SM(Reimar):** Tilføjet kort samt login/register-side til vores flutter app
|
||||
|
||||
**PO(Philip):** start på menu som kunderne bad om
|
||||
|
||||
## Onsdag
|
||||
|
||||
**KC(Alexander):** Mødte lidt sent, kiggede Reimar's kode igennem, pointerede et issue med email-validation (kan nok løses med lidt Regex)
|
||||
|
||||
**SM(Reimar):** Koblet flutter app sammen med vorese backend API til login/signup
|
||||
|
||||
**PO(Philip):** færdig med side-menu som funger på alle sider.
|
||||
|
||||
## Torsdag
|
||||
|
||||
**KC(Alexander):**
|
||||
|
||||
**SM(Reimar):**
|
||||
|
||||
**PO(Philip):**
|
||||
|
||||
## Fredag
|
||||
|
||||
**KC(Alexander):**
|
||||
|
||||
**SM(Reimar):**
|
||||
|
||||
**PO(Philip):**
|
||||
|
||||
|
6
Mobile/.gitignore
vendored
6
Mobile/.gitignore
vendored
@ -29,9 +29,7 @@ migrate_working_dir/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
#/build/e514a05cd739d52d8e7038a04d724576
|
||||
#/build/flutter_assets
|
||||
#/build/2794971a5855e7decd7bb368de5d49d1.cache.dill.track.dill
|
||||
build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
@ -43,3 +41,5 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
environment.json
|
||||
|
7
Mobile/.run/main.dart.run.xml
Normal file
7
Mobile/.run/main.dart.run.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
|
||||
<option name="additionalArgs" value="--dart-define-from-file environment.json" />
|
||||
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
15
Mobile/.vscode/launch.json
vendored
Normal file
15
Mobile/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": [
|
||||
"--dart-define-from-file",
|
||||
"environment.json"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
6
Mobile/README.md
Normal file
6
Mobile/README.md
Normal file
@ -0,0 +1,6 @@
|
||||
# SkanTravels frontend
|
||||
|
||||
### Running
|
||||
|
||||
- Copy `environment.example.json` to `environment.json` and fill out the values
|
||||
- Run `flutter run --dart-define-from-file environment.json`
|
@ -1,4 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:label="mobile"
|
||||
android:name="${applicationName}"
|
||||
|
4
Mobile/environment.example.json
Normal file
4
Mobile/environment.example.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"AUTH_SERVICE_HOST": "http://localhost:5287",
|
||||
"APP_SERVICE_HOST": "http://localhost:8080"
|
||||
}
|
81
Mobile/lib/api.dart
Normal file
81
Mobile/lib/api.dart
Normal file
@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
|
||||
enum ApiService {
|
||||
auth,
|
||||
app,
|
||||
}
|
||||
|
||||
Future<String?> request(BuildContext context, ApiService service, String method, String path, Object? body) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
|
||||
final host = switch (service) {
|
||||
ApiService.auth => const String.fromEnvironment('AUTH_SERVICE_HOST'),
|
||||
ApiService.app => const String.fromEnvironment('APP_SERVICE_HOST'),
|
||||
};
|
||||
|
||||
final http.Response response;
|
||||
|
||||
try {
|
||||
if (method == 'GET') {
|
||||
response = await http.get(Uri.parse(host + path));
|
||||
} else {
|
||||
final function = switch (method) {
|
||||
'POST' => http.post,
|
||||
'PUT' => http.put,
|
||||
'DELETE' => http.delete,
|
||||
_ => throw const FormatException('Invalid method'),
|
||||
};
|
||||
|
||||
response = await function(
|
||||
Uri.parse(host + path),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: body != null ? jsonEncode(body) : null,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
messenger.showSnackBar(const SnackBar(content: Text('Unable to connect to server')));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
try {
|
||||
final json = jsonDecode(response.body);
|
||||
messenger.showSnackBar(SnackBar(content: Text(json['message'])));
|
||||
} catch (_) {
|
||||
messenger.showSnackBar(SnackBar(content: Text('Something went wrong (HTTP ${response.statusCode})')));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.body;
|
||||
}
|
||||
|
||||
Future<bool> isLoggedIn(BuildContext context) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final token = prefs.getString('token');
|
||||
if (token == null) return false;
|
||||
|
||||
try {
|
||||
String base64 = token.split('.')[1];
|
||||
base64 += List.filled(4 - base64.length % 4, '=').join();
|
||||
|
||||
final payload = jsonDecode(String.fromCharCodes(base64Decode(base64)));
|
||||
|
||||
if (payload['exp'] < DateTime.now().millisecondsSinceEpoch / 1000) {
|
||||
messenger.showSnackBar(const SnackBar(content: Text('Token expired, please sign in again')));
|
||||
prefs.remove('token');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(const SnackBar(content: Text('Invalid token, please sign in again')));
|
||||
prefs.remove('token');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
104
Mobile/lib/base/sidemenu.dart
Normal file
104
Mobile/lib/base/sidemenu.dart
Normal file
@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SideMenu extends StatefulWidget {
|
||||
final Widget body;
|
||||
|
||||
const SideMenu({Key? key, required this.body}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SideMenu> createState() => _SideMenuState();
|
||||
}
|
||||
|
||||
class _SideMenuState extends State<SideMenu> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
void _onItemTapped(int index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: const Text('SkanTavels'),
|
||||
),
|
||||
drawer: Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
const DrawerHeader(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
),
|
||||
child: Text('Drawer Header'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Home'),
|
||||
leading: const Icon(Icons.home),
|
||||
selected: _selectedIndex == 0,
|
||||
onTap: () {
|
||||
// Update the state of the app
|
||||
_onItemTapped(0);
|
||||
// Then close the drawer
|
||||
Navigator.pushReplacementNamed(context, '/home');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Favourites'),
|
||||
leading: const Icon(Icons.star),
|
||||
selected: _selectedIndex == 1,
|
||||
onTap: () {
|
||||
// Update the state of the app
|
||||
_onItemTapped(1);
|
||||
// Then close the drawer
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Profile'),
|
||||
leading: const Icon(Icons.person),
|
||||
selected: _selectedIndex == 2,
|
||||
onTap: () {
|
||||
// Update the state of the app
|
||||
_onItemTapped(2);
|
||||
// Then close the drawer
|
||||
Navigator.pushReplacementNamed(context, '/profile');
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
color: Colors.grey,
|
||||
thickness: 2,
|
||||
indent: 40,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Register'),
|
||||
leading: const Icon(Icons.add_box_outlined),
|
||||
selected: _selectedIndex == 3,
|
||||
onTap: () {
|
||||
// Update the state of the app
|
||||
_onItemTapped(3);
|
||||
// Then close the drawer
|
||||
Navigator.pushReplacementNamed(context, '/register');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Login'),
|
||||
leading: const Icon(Icons.login),
|
||||
selected: _selectedIndex == 4,
|
||||
onTap: () {
|
||||
// Update the state of the app
|
||||
_onItemTapped(4);
|
||||
// Then close the drawer
|
||||
Navigator.pushReplacementNamed(context, '/login');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: widget.body,
|
||||
);
|
||||
}
|
||||
}
|
83
Mobile/lib/login.dart
Normal file
83
Mobile/lib/login.dart
Normal file
@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile/base/sidemenu.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'register.dart';
|
||||
import 'api.dart' as api;
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key, required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final emailInput = TextEditingController();
|
||||
final passwordInput = TextEditingController();
|
||||
|
||||
Future<void> _login() async {
|
||||
final token = await api
|
||||
.request(context, api.ApiService.auth, 'POST', '/api/Users/login', {
|
||||
'email': emailInput.text,
|
||||
'password': passwordInput.text,
|
||||
});
|
||||
|
||||
if (token == null) return;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setString('token', token);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Successfully logged in')));
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SideMenu(
|
||||
body: Scaffold(
|
||||
body: Center(
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: 100, maxWidth: 400),
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 80),
|
||||
const Text('Email'),
|
||||
TextField(controller: emailInput),
|
||||
const SizedBox(height: 30),
|
||||
const Text('Password'),
|
||||
TextField(
|
||||
controller: passwordInput,
|
||||
obscureText: true,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false),
|
||||
const SizedBox(height: 30),
|
||||
ElevatedButton(onPressed: _login, child: const Text('Login')),
|
||||
const SizedBox(height: 10),
|
||||
TextButton(
|
||||
child: const Text('Register account'),
|
||||
onPressed: () => Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const RegisterPage(title: 'Register')))),
|
||||
ElevatedButton(onPressed: _login, child: const Text('Log ind')),
|
||||
const SizedBox(height: 10),
|
||||
TextButton(
|
||||
child: const Text('Registrer konto'),
|
||||
onPressed: () => Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const RegisterPage(title: 'Registrer'))),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
emailInput.dispose();
|
||||
passwordInput.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'weather.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:mobile/register.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'login.dart';
|
||||
import 'api.dart' as api;
|
||||
import 'base/sidemenu.dart';
|
||||
import "login.dart";
|
||||
import 'profile.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
@ -7,32 +15,24 @@ void main() {
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'H4 Flutter',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const MyHomePage(title: 'H4 med Flutter'),
|
||||
home: const MyHomePage(title: 'SkanTravels'),
|
||||
initialRoute: '/',
|
||||
routes: {
|
||||
'/home': (context) => const MyHomePage(
|
||||
title: 'SkasdanTravels',
|
||||
),
|
||||
'/profile': (context) => const ProfilePage(),
|
||||
'/login': (context) => const LoginPage(title: 'SkanTravels'),
|
||||
'/register': (context) => const RegisterPage(title: 'SkanTravels'),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -40,15 +40,6 @@ class MyApp extends StatelessWidget {
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
@ -56,63 +47,182 @@ class MyHomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
int _selectedIndex = 0;
|
||||
bool _isLoggedIn = false;
|
||||
|
||||
void _incrementCounter() {
|
||||
void _onItemTapped(int index) {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
_selectedIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
void _navigateToWeatherForecastPage() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => WeatherForecastPage()),
|
||||
);
|
||||
void _logout() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
prefs.remove('token');
|
||||
setState(() => _isLoggedIn = false);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Successfully logged out')));
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _postNavigationCallback(dynamic _) async {
|
||||
final isLoggedIn = await api.isLoggedIn(context);
|
||||
setState(() => _isLoggedIn = isLoggedIn);
|
||||
|
||||
// Close sidebar
|
||||
if (mounted && _scaffoldKey.currentState?.isDrawerOpen == true) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
api.isLoggedIn(context)
|
||||
.then((value) => setState(() => _isLoggedIn = value));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'You have clicked the button this many times:',
|
||||
),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
return SideMenu(
|
||||
body: Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
),
|
||||
drawer: navigationMenu,
|
||||
body: FlutterMap(
|
||||
options: const MapOptions(
|
||||
initialCenter: LatLng(55.9397, 9.5156), initialZoom: 7.0),
|
||||
children: [
|
||||
openStreetMapTileLayer,
|
||||
const MarkerLayer(markers: [
|
||||
Marker(
|
||||
point: LatLng(56.465511, 9.411366),
|
||||
width: 60,
|
||||
height: 100,
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
Icons.location_pin,
|
||||
size: 60,
|
||||
color: Colors.purple,
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
floatingActionButton: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
children: _isLoggedIn ? [] : [
|
||||
FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
SizedBox(width: 10), // Optional: Adds space between buttons
|
||||
FloatingActionButton(
|
||||
onPressed: _navigateToWeatherForecastPage,
|
||||
tooltip: 'Show Weather',
|
||||
child: const Icon(Icons.star),
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const LoginPage(title: "Login")))
|
||||
.then(_postNavigationCallback);
|
||||
},
|
||||
tooltip: 'Login',
|
||||
child: const Icon(Icons.login),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Drawer get navigationMenu => Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
const DrawerHeader(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
),
|
||||
child: Text('Drawer Header'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Home'),
|
||||
leading: const Icon(Icons.home),
|
||||
selected: _selectedIndex == 0,
|
||||
onTap: () {
|
||||
// Update the state of the app
|
||||
_onItemTapped(0);
|
||||
// Then close the drawer
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Favourites'),
|
||||
leading: const Icon(Icons.star),
|
||||
selected: _selectedIndex == 1,
|
||||
onTap: () {
|
||||
// Update the state of the app
|
||||
_onItemTapped(1);
|
||||
// Then close the drawer
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Profile'),
|
||||
leading: const Icon(Icons.person),
|
||||
selected: _selectedIndex == 2,
|
||||
onTap: () {
|
||||
// Update the state of the app
|
||||
_onItemTapped(2);
|
||||
// Then close the drawer
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
color: Colors.grey,
|
||||
thickness: 2,
|
||||
indent: 40,
|
||||
),
|
||||
...(
|
||||
_isLoggedIn ? [
|
||||
ListTile(
|
||||
title: const Text('Log out'),
|
||||
leading: const Icon(Icons.logout),
|
||||
selected: false,
|
||||
onTap: _logout,
|
||||
)
|
||||
] : [
|
||||
ListTile(
|
||||
title: const Text('Register'),
|
||||
leading: const Icon(Icons.add_box_outlined),
|
||||
selected: _selectedIndex == 3,
|
||||
onTap: () {
|
||||
// Update the state of the app
|
||||
_onItemTapped(3);
|
||||
// Then close the drawer
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterPage(title: 'Register')))
|
||||
.then(_postNavigationCallback);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Login'),
|
||||
leading: const Icon(Icons.login),
|
||||
selected: _selectedIndex == 4,
|
||||
onTap: () {
|
||||
// Update the state of the app
|
||||
_onItemTapped(4);
|
||||
// Then close the drawer
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const LoginPage(title: 'Login')))
|
||||
.then(_postNavigationCallback);
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
TileLayer get openStreetMapTileLayer => TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
|
||||
);
|
||||
}
|
||||
|
15
Mobile/lib/profile.dart
Normal file
15
Mobile/lib/profile.dart
Normal file
@ -0,0 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'base/sidemenu.dart'; // Import the base layout widget
|
||||
|
||||
class ProfilePage extends StatelessWidget {
|
||||
const ProfilePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SideMenu(
|
||||
body: Center(
|
||||
child: Text('This is Page 1'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
83
Mobile/lib/register.dart
Normal file
83
Mobile/lib/register.dart
Normal file
@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile/base/sidemenu.dart';
|
||||
import 'login.dart';
|
||||
import 'api.dart' as api;
|
||||
|
||||
class RegisterPage extends StatefulWidget {
|
||||
const RegisterPage({super.key, required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<RegisterPage> createState() => _RegisterPageState();
|
||||
}
|
||||
|
||||
class _RegisterPageState extends State<RegisterPage> {
|
||||
final usernameInput = TextEditingController();
|
||||
final emailInput = TextEditingController();
|
||||
final passwordInput = TextEditingController();
|
||||
|
||||
Future<void> _register() async {
|
||||
final result =
|
||||
await api.request(context, api.ApiService.auth, 'POST', '/api/Users', {
|
||||
'username': usernameInput.text,
|
||||
'email': emailInput.text,
|
||||
'password': passwordInput.text,
|
||||
});
|
||||
|
||||
if (result == null) return;
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Successfully registered, please login')));
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const LoginPage(title: 'Log ind')));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SideMenu(
|
||||
body: Scaffold(
|
||||
body: Center(
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: 100, maxWidth: 400),
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 80),
|
||||
const Text('Username'),
|
||||
TextField(controller: usernameInput),
|
||||
const SizedBox(height: 30),
|
||||
const Text('Email'),
|
||||
TextField(controller: emailInput),
|
||||
const SizedBox(height: 30),
|
||||
const Text('Password'),
|
||||
TextField(
|
||||
controller: passwordInput,
|
||||
obscureText: true,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false),
|
||||
const SizedBox(height: 30),
|
||||
ElevatedButton(onPressed: _register, child: const Text('Register')),
|
||||
const SizedBox(height: 10),
|
||||
TextButton(
|
||||
child: const Text('Login'),
|
||||
onPressed: () => Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const LoginPage(title: 'Login')))
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
usernameInput.dispose();
|
||||
emailInput.dispose();
|
||||
passwordInput.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
|
||||
class WeatherForecastPage extends StatefulWidget {
|
||||
@override
|
||||
_WeatherForecastPageState createState() => _WeatherForecastPageState();
|
||||
}
|
||||
|
||||
class _WeatherForecastPageState extends State<WeatherForecastPage> {
|
||||
List<dynamic> forecasts = [];
|
||||
List<double> temperaturesC = [];
|
||||
List<String> dates = [];
|
||||
|
||||
Future<void> fetchForecasts() async {
|
||||
final response =
|
||||
await http.get(Uri.parse('https://h4api.onrender.com/WeatherForecast'));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
setState(() {
|
||||
temperaturesC.clear();
|
||||
dates.clear();
|
||||
forecasts = json.decode(response.body).map((f) {
|
||||
temperaturesC.add(f['temperatureC'].toDouble());
|
||||
dates.add(f['date']);
|
||||
return f;
|
||||
}).toList();
|
||||
});
|
||||
} else {
|
||||
throw Exception('Failed to load forecasts');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fetchForecasts();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Weather Forecast Graph'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
onPressed: fetchForecasts,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: forecasts.isEmpty
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minY:
|
||||
(temperaturesC.reduce((a, b) => a < b ? a : b) - 10)
|
||||
.toDouble(),
|
||||
maxY:
|
||||
(temperaturesC.reduce((a, b) => a > b ? a : b) + 20)
|
||||
.toDouble(),
|
||||
gridData: FlGridData(show: true), // Grid
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
getTextStyles: (context, value) => const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
interval: 10,
|
||||
getTitles: (value) {
|
||||
return '${value.toInt()}°C';
|
||||
},
|
||||
),
|
||||
bottomTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
getTextStyles: (context, value) => const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
getTitles: (value) {
|
||||
int index = value.toInt();
|
||||
if (index >= 0 && index < dates.length) {
|
||||
return dates[index].substring(5); // MM-DD
|
||||
}
|
||||
return '';
|
||||
},
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: Border.all(color: Colors.black, width: 1),
|
||||
),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: temperaturesC.asMap().entries.map((e) {
|
||||
return FlSpot(e.key.toDouble(), e.value);
|
||||
}).toList(),
|
||||
isCurved: true,
|
||||
colors: [Colors.blue],
|
||||
barWidth: 4,
|
||||
dotData: FlDotData(show: true),
|
||||
belowBarData: BarAreaData(show: false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'Weather Summaries',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: forecasts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final forecast = forecasts[index];
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'${forecast['date']}: ${forecast['summary']}',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: fetchForecasts,
|
||||
child: Text('Refresh Data'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import shared_preferences_foundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
}
|
||||
|
@ -8,5 +8,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -4,5 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -49,14 +49,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
equatable:
|
||||
dart_earcut:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
|
||||
name: dart_earcut
|
||||
sha256: "41b493147e30a051efb2da1e3acb7f38fe0db60afba24ac1ea5684cee272721e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "1.1.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -65,14 +65,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
fl_chart:
|
||||
dependency: "direct main"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fl_chart
|
||||
sha256: "0c8baa9d4db70817d27099efb99e40e4164448bb86f73217216ef65b3976bc4f"
|
||||
name: ffi
|
||||
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.36.4"
|
||||
version: "2.1.3"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -86,19 +94,32 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
flutter_map:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_map
|
||||
sha256: "2ecb34619a4be19df6f40c2f8dce1591675b4eff7a6857bd8f533706977385da"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
|
||||
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.6"
|
||||
version: "1.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -107,30 +128,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
latlong2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: latlong2
|
||||
sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
version: "10.0.5"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.5"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.1"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -139,6 +176,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
lists:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lists
|
||||
sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logger
|
||||
sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -151,18 +204,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
version: "1.15.0"
|
||||
mgrs_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mgrs_dart
|
||||
sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -171,14 +232,118 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
pedantic:
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pedantic
|
||||
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
polylabel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: polylabel
|
||||
sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
proj4dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: proj4dart
|
||||
sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.2"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -228,10 +393,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
version: "0.7.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -240,6 +405,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
unicode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: unicode
|
||||
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -252,9 +425,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
||||
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
version: "14.2.4"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
wkt_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wkt_parser
|
||||
sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
sdks:
|
||||
dart: ">=3.3.4 <4.0.0"
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
|
@ -30,12 +30,14 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
http: ^0.13.3
|
||||
fl_chart: ^0.36.0
|
||||
http: ^1.2.1
|
||||
flutter_map: ^7.0.2
|
||||
latlong2: ^0.9.1
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.6
|
||||
shared_preferences: ^2.3.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -1,16 +0,0 @@
|
||||
# mobile
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
@ -1,7 +1,7 @@
|
||||
using API.Models;
|
||||
using AuthorizationService.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API;
|
||||
namespace AuthorizationService;
|
||||
|
||||
public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(options)
|
||||
{
|
@ -1,9 +1,9 @@
|
||||
using API.Models;
|
||||
using API.Persistence.Repositories;
|
||||
using AuthorizationService.Models;
|
||||
using AuthorizationService.Persistence.Repositories;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace API.Application.Users.Commands
|
||||
namespace AuthorizationService.Application.Users.Commands
|
||||
{
|
||||
public class CreateUser
|
||||
{
|
||||
@ -77,9 +77,6 @@ namespace API.Application.Users.Commands
|
||||
CreatedAt = DateTime.UtcNow.AddHours(2),
|
||||
UpdatedAt = DateTime.UtcNow.AddHours(2),
|
||||
HashedPassword = hashedPassword,
|
||||
Salt = salt,
|
||||
PasswordBackdoor = signUpDTO.Password,
|
||||
// Only for educational purposes, not in the final product!
|
||||
};
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using API.Models;
|
||||
using API.Persistence.Repositories;
|
||||
using AuthorizationService.Models;
|
||||
using AuthorizationService.Persistence.Repositories;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Application.Users.Commands
|
||||
namespace AuthorizationService.Application.Users.Commands
|
||||
{
|
||||
public class DeleteUser
|
||||
{
|
@ -1,5 +1,5 @@
|
||||
using API.Models;
|
||||
using API.Persistence.Repositories;
|
||||
using AuthorizationService.Models;
|
||||
using AuthorizationService.Persistence.Repositories;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
@ -9,7 +9,7 @@ using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
namespace API.Application.Users.Commands
|
||||
namespace AuthorizationService.Application.Users.Commands
|
||||
{
|
||||
public class LoginUser
|
||||
{
|
@ -1,8 +1,8 @@
|
||||
using API.Models;
|
||||
using API.Persistence.Repositories;
|
||||
using AuthorizationService.Models;
|
||||
using AuthorizationService.Persistence.Repositories;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Application.Users.Commands
|
||||
namespace AuthorizationService.Application.Users.Commands
|
||||
{
|
||||
public class UpdateUser
|
||||
{
|
@ -0,0 +1,57 @@
|
||||
using AuthorizationService.Models;
|
||||
using AuthorizationService.Persistence.Repositories;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AuthorizationService.Application.Users.Commands
|
||||
{
|
||||
public class UpdateUserPassword
|
||||
{
|
||||
private readonly IUserRepository _repository;
|
||||
|
||||
public UpdateUserPassword(IUserRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Handle(ChangePasswordDTO changePasswordDTO)
|
||||
{
|
||||
if (!IsPasswordSecure(changePasswordDTO.NewPassword))
|
||||
{
|
||||
return new ConflictObjectResult(new { message = "New Password is not secure." });
|
||||
}
|
||||
|
||||
User currentUser = await _repository.QueryUserByIdAsync(changePasswordDTO.Id);
|
||||
if (currentUser == null || !BCrypt.Net.BCrypt.Verify(changePasswordDTO.OldPassword, currentUser.HashedPassword))
|
||||
{
|
||||
return new UnauthorizedObjectResult(new { message = "Old Password is incorrect" });
|
||||
}
|
||||
string hashedPassword = BCrypt.Net.BCrypt.HashPassword(changePasswordDTO.NewPassword);
|
||||
|
||||
currentUser.HashedPassword = hashedPassword;
|
||||
|
||||
bool success = await _repository.UpdateUserPasswordAsync(currentUser);
|
||||
if (success)
|
||||
return new OkResult();
|
||||
else
|
||||
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
|
||||
private bool IsPasswordSecure(string password)
|
||||
{
|
||||
var hasUpperCase = new Regex(@"[A-Z]+");
|
||||
var hasLowerCase = new Regex(@"[a-z]+");
|
||||
var hasDigits = new Regex(@"[0-9]+");
|
||||
var hasSpecialChar = new Regex(@"[\W_]+");
|
||||
var hasMinimum8Chars = new Regex(@".{8,}");
|
||||
|
||||
return hasUpperCase.IsMatch(password)
|
||||
&& hasLowerCase.IsMatch(password)
|
||||
&& hasDigits.IsMatch(password)
|
||||
&& hasSpecialChar.IsMatch(password)
|
||||
&& hasMinimum8Chars.IsMatch(password);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using API.Models;
|
||||
using API.Persistence.Repositories;
|
||||
using AuthorizationService.Models;
|
||||
using AuthorizationService.Persistence.Repositories;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Application.Users.Queries
|
||||
namespace AuthorizationService.Application.Users.Queries
|
||||
{
|
||||
public class QueryAllUsers
|
||||
{
|
@ -1,8 +1,9 @@
|
||||
using API.Models;
|
||||
using API.Persistence.Repositories;
|
||||
using AuthorizationService.Models;
|
||||
using AuthorizationService.Persistence.Repositories;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages.Manage;
|
||||
|
||||
namespace API.Application.Users.Queries
|
||||
namespace AuthorizationService.Application.Users.Queries
|
||||
{
|
||||
public class QueryUserById
|
||||
{
|
||||
@ -13,10 +14,15 @@ namespace API.Application.Users.Queries
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<UserDTO> Handle(string id)
|
||||
public async Task<ActionResult<UserDTO>> Handle(string id)
|
||||
{
|
||||
User user = await _repository.QueryUserByIdAsync(id);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return new ConflictObjectResult(new { message = "No user on given Id" });
|
||||
}
|
||||
|
||||
UserDTO userDTO = new UserDTO
|
||||
{
|
||||
Id = user.Id,
|
@ -9,7 +9,7 @@
|
||||
<WebStackScaffolding_IsPartialViewSelected>False</WebStackScaffolding_IsPartialViewSelected>
|
||||
<WebStackScaffolding_IsReferencingScriptLibrariesSelected>True</WebStackScaffolding_IsReferencingScriptLibrariesSelected>
|
||||
<WebStackScaffolding_LayoutPageFile />
|
||||
<WebStackScaffolding_DbContextTypeFullName>API.AppDBContext</WebStackScaffolding_DbContextTypeFullName>
|
||||
<WebStackScaffolding_DbContextTypeFullName>AuthorizationService.AppDBContext</WebStackScaffolding_DbContextTypeFullName>
|
||||
<WebStackScaffolding_IsAsyncSelected>False</WebStackScaffolding_IsAsyncSelected>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.8.34511.84
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API.csproj", "{5DF9B7D8-FA4E-4209-A677-C4CF4886D4B3}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorizationService", "AuthorizationService.csproj", "{5DF9B7D8-FA4E-4209-A677-C4CF4886D4B3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace AuthorizationService.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
@ -1,6 +1,6 @@
|
||||
using API.Application.Users.Commands;
|
||||
using API.Application.Users.Queries;
|
||||
using API.Models;
|
||||
using AuthorizationService.Application.Users.Commands;
|
||||
using AuthorizationService.Application.Users.Queries;
|
||||
using AuthorizationService.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -10,7 +10,7 @@ using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace API.Controllers
|
||||
namespace AuthorizationService.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
@ -20,6 +20,7 @@ namespace API.Controllers
|
||||
private readonly QueryUserById _queryUserById;
|
||||
private readonly CreateUser _createUser;
|
||||
private readonly UpdateUser _updateUser;
|
||||
private readonly UpdateUserPassword _updateUserPassword;
|
||||
private readonly DeleteUser _deleteUser;
|
||||
private readonly LoginUser _loginUser;
|
||||
|
||||
@ -28,6 +29,7 @@ namespace API.Controllers
|
||||
QueryUserById queryUserById,
|
||||
CreateUser createUser,
|
||||
UpdateUser updateUser,
|
||||
UpdateUserPassword updateUserPassword,
|
||||
DeleteUser deleteUser,
|
||||
LoginUser loginUser)
|
||||
{
|
||||
@ -35,18 +37,17 @@ namespace API.Controllers
|
||||
_queryUserById = queryUserById;
|
||||
_createUser = createUser;
|
||||
_updateUser = updateUser;
|
||||
_updateUserPassword = updateUserPassword;
|
||||
_deleteUser = deleteUser;
|
||||
_loginUser = loginUser;
|
||||
}
|
||||
|
||||
// POST: api/Users/login
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login(LoginDTO login)
|
||||
{
|
||||
return await _loginUser.Handle(login);
|
||||
}
|
||||
|
||||
// GET: api/Users
|
||||
[Authorize]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<UserDTO>>> GetUsers()
|
||||
@ -54,8 +55,7 @@ namespace API.Controllers
|
||||
return await _queryAllUsers.Handle();
|
||||
}
|
||||
|
||||
// GET: api/Users/5
|
||||
[Authorize]
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<UserDTO>> GetUser(string id)
|
||||
{
|
||||
@ -63,25 +63,26 @@ namespace API.Controllers
|
||||
|
||||
}
|
||||
|
||||
// PUT: api/Users/5
|
||||
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
|
||||
[Authorize]
|
||||
[HttpPut("{id}")]
|
||||
[HttpPut]
|
||||
public async Task<IActionResult> PutUser(UserDTO userDTO)
|
||||
{
|
||||
return await _updateUser.Handle(userDTO);
|
||||
}
|
||||
|
||||
// POST: api/Users
|
||||
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
|
||||
[Authorize]
|
||||
[HttpPut("password")]
|
||||
public async Task<IActionResult> PutUserPassword(ChangePasswordDTO changePasswordDTO)
|
||||
{
|
||||
return await _updateUserPassword.Handle(changePasswordDTO);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Guid>> PostUser(SignUpDTO signUpDTO)
|
||||
{
|
||||
return await _createUser.Handle(signUpDTO);
|
||||
}
|
||||
|
||||
|
||||
// DELETE: api/Users/5
|
||||
[Authorize]
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteUser(string id)
|
@ -9,17 +9,17 @@ EXPOSE 8081
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["API.csproj", "."]
|
||||
RUN dotnet restore "./././API.csproj"
|
||||
COPY ["AuthorizationService.csproj", "."]
|
||||
RUN dotnet restore "./././AuthorizationService.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/."
|
||||
RUN dotnet build "./API.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
RUN dotnet build "./AuthorizationService.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
RUN dotnet publish "./AuthorizationService.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "API.dll"]
|
||||
ENTRYPOINT ["dotnet", "AuthorizationService.dll"]
|
@ -1,5 +1,5 @@
|
||||
// <auto-generated />
|
||||
using API;
|
||||
using AuthorizationService;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
namespace AuthorizationService.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDBContext))]
|
||||
[Migration("20240812084720_CreateUser")]
|
||||
@ -19,7 +19,7 @@ namespace API.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.0-preview.6.24327.4");
|
||||
|
||||
modelBuilder.Entity("API.Models.User", b =>
|
||||
modelBuilder.Entity("AuthorizationService.Models.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
@ -2,7 +2,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
namespace AuthorizationService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class CreateUser : Migration
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API;
|
||||
using AuthorizationService;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
namespace AuthorizationService.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDBContext))]
|
||||
[Migration("20240813075158_ChangedUserWithGuid")]
|
||||
@ -20,7 +20,7 @@ namespace API.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
|
||||
|
||||
modelBuilder.Entity("API.Models.User", b =>
|
||||
modelBuilder.Entity("AuthorizationService.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
namespace AuthorizationService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ChangedUserWithGuid : Migration
|
@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API;
|
||||
using AuthorizationService;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
namespace AuthorizationService.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDBContext))]
|
||||
[Migration("20240813112418_NoMoreGuid")]
|
||||
@ -20,7 +20,7 @@ namespace API.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
|
||||
|
||||
modelBuilder.Entity("API.Models.User", b =>
|
||||
modelBuilder.Entity("AuthorizationService.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
@ -2,7 +2,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
namespace AuthorizationService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class NoMoreGuid : Migration
|
54
services/AuthorizationService/Migrations/20240816102314_removedSaltAndBackdoor.Designer.cs
generated
Normal file
54
services/AuthorizationService/Migrations/20240816102314_removedSaltAndBackdoor.Designer.cs
generated
Normal file
@ -0,0 +1,54 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDBContext))]
|
||||
[Migration("20240816102314_removedSaltAndBackdoor")]
|
||||
partial class removedSaltAndBackdoor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
|
||||
|
||||
modelBuilder.Entity("API.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("HashedPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class removedSaltAndBackdoor : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PasswordBackdoor",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Salt",
|
||||
table: "Users");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PasswordBackdoor",
|
||||
table: "Users",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Salt",
|
||||
table: "Users",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API;
|
||||
using AuthorizationService;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
namespace AuthorizationService.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDBContext))]
|
||||
partial class AppDBContextModelSnapshot : ModelSnapshot
|
||||
@ -17,7 +17,7 @@ namespace API.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
|
||||
|
||||
modelBuilder.Entity("API.Models.User", b =>
|
||||
modelBuilder.Entity("AuthorizationService.Models.User", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
@ -35,14 +35,6 @@ namespace API.Migrations
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordBackdoor")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Salt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
@ -1,4 +1,4 @@
|
||||
namespace API.Models
|
||||
namespace AuthorizationService.Models
|
||||
{
|
||||
public class BaseModel
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Models;
|
||||
namespace AuthorizationService.Models;
|
||||
|
||||
public class User : BaseModel
|
||||
{
|
||||
@ -8,8 +8,6 @@ public class User : BaseModel
|
||||
public string? Username { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public string HashedPassword { get; set; }
|
||||
public string PasswordBackdoor { get; set; }
|
||||
public string Salt { get; set; }
|
||||
}
|
||||
|
||||
public class UserDTO
|
||||
@ -30,5 +28,12 @@ public class SignUpDTO
|
||||
public string Email { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class ChangePasswordDTO
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string OldPassword { get; set; }
|
||||
public string NewPassword { get; set; }
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
using API.Models;
|
||||
using AuthorizationService.Models;
|
||||
|
||||
namespace API.Persistence.Repositories
|
||||
namespace AuthorizationService.Persistence.Repositories
|
||||
{
|
||||
public interface IUserRepository
|
||||
{
|
||||
@ -10,5 +10,6 @@ namespace API.Persistence.Repositories
|
||||
Task<User> QueryUserByIdAsync(string id);
|
||||
Task<User> QueryUserByEmailAsync(string email);
|
||||
Task<bool> UpdateUserAsync(User user);
|
||||
Task<bool> UpdateUserPasswordAsync(User user);
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using API.Models;
|
||||
using AuthorizationService.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages;
|
||||
|
||||
namespace API.Persistence.Repositories
|
||||
namespace AuthorizationService.Persistence.Repositories
|
||||
{
|
||||
public class UserRepository(AppDBContext context) : IUserRepository
|
||||
{
|
||||
@ -21,7 +21,6 @@ namespace API.Persistence.Repositories
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
return new User();
|
||||
}
|
||||
|
||||
@ -58,6 +57,21 @@ namespace API.Persistence.Repositories
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateUserPasswordAsync(User user)
|
||||
{
|
||||
try
|
||||
{
|
||||
_context.Entry(user).State = EntityState.Modified;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteUserAsync(string id)
|
||||
{
|
||||
var user = await _context.Users.FindAsync(id);
|
@ -1,13 +1,13 @@
|
||||
using API.Application.Users.Commands;
|
||||
using API.Application.Users.Queries;
|
||||
using API.Persistence.Repositories;
|
||||
using AuthorizationService.Application.Users.Commands;
|
||||
using AuthorizationService.Application.Users.Queries;
|
||||
using AuthorizationService.Persistence.Repositories;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using System.Text;
|
||||
|
||||
namespace API
|
||||
namespace AuthorizationService
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
@ -36,6 +36,7 @@ namespace API
|
||||
builder.Services.AddScoped<QueryUserById>();
|
||||
builder.Services.AddScoped<CreateUser>();
|
||||
builder.Services.AddScoped<UpdateUser>();
|
||||
builder.Services.AddScoped<UpdateUserPassword>();
|
||||
builder.Services.AddScoped<DeleteUser>();
|
||||
builder.Services.AddScoped<LoginUser>();
|
||||
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
@ -7,7 +7,7 @@ Environment=DOTNET_ROOT=/home/reimar/.dotnet
|
||||
Environment=PATH=$PATH:/home/reimar/.dotnet
|
||||
Environment=DEFAULT_CONNECTION="Data Source=/home/reimar/skantravels/database.sqlite3"
|
||||
ExecStartPre=/home/reimar/skantravels/efbundle
|
||||
ExecStart=/home/reimar/skantravels/API --urls=http://0.0.0.0:5001
|
||||
ExecStart=/home/reimar/skantravels/AuthorizationService --urls=http://0.0.0.0:5001
|
||||
Type=simple
|
||||
|
||||
[Install]
|
@ -1,10 +1,11 @@
|
||||
mod auth;
|
||||
mod models;
|
||||
|
||||
use actix_web::{get, Responder, HttpResponse, HttpServer, App, web};
|
||||
use actix_web::{get, post, delete, Responder, HttpResponse, HttpServer, App, web};
|
||||
use std::sync::{Mutex, MutexGuard, Arc};
|
||||
use auth::AuthorizedUser;
|
||||
use models::Favorite;
|
||||
use serde::Deserialize;
|
||||
|
||||
mod embedded {
|
||||
use refinery::embed_migrations;
|
||||
@ -51,6 +52,49 @@ fn get_favorites(db: MutexGuard<'_, rusqlite::Connection>, user_id: String) -> O
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateFavoriteRequest {
|
||||
lat: f64,
|
||||
lng: f64,
|
||||
}
|
||||
|
||||
#[post("/favorites")]
|
||||
async fn create_favorite(auth: AuthorizedUser, data: web::Data<AppData>, input: web::Json<CreateFavoriteRequest>) -> impl Responder {
|
||||
let db = data.database.lock().unwrap();
|
||||
|
||||
match db.execute(
|
||||
"INSERT INTO favorites (user_id, lat, lng) VALUES (:user_id, :lat, :lng)",
|
||||
&[(":user_id", &auth.user_id), (":lat", &input.lat.to_string()), (":lng", &input.lng.to_string())]
|
||||
) {
|
||||
Ok(_) => HttpResponse::Created(),
|
||||
Err(_) => HttpResponse::InternalServerError(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/favorites/{favorite}")]
|
||||
async fn delete_favorite(auth: AuthorizedUser, data:web::Data<AppData>, path: web::Path<usize>) -> impl Responder {
|
||||
let db = data.database.lock().unwrap();
|
||||
let favorite_id = path.into_inner();
|
||||
let params = &[(":id", &favorite_id.to_string())];
|
||||
|
||||
let result = db.query_row("SELECT * FROM favorites WHERE id = :id LIMIT 1", params, |row| Favorite::from_row(row));
|
||||
|
||||
if result.is_err() {
|
||||
return HttpResponse::InternalServerError().finish();
|
||||
}
|
||||
|
||||
let favorite = result.unwrap();
|
||||
|
||||
if favorite.user_id != auth.user_id {
|
||||
return HttpResponse::Forbidden().body("Cannot remove favorite that you did not create");
|
||||
}
|
||||
|
||||
match db.execute("DELETE FROM favorites WHERE id = :id", params) {
|
||||
Ok(_) => HttpResponse::NoContent().finish(),
|
||||
Err(_) => HttpResponse::InternalServerError().finish(),
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let _ = dotenvy::dotenv();
|
||||
@ -81,6 +125,8 @@ async fn main() -> std::io::Result<()> {
|
||||
.service(healthcheck)
|
||||
.service(authorized)
|
||||
.service(favorites)
|
||||
.service(create_favorite)
|
||||
.service(delete_favorite)
|
||||
})
|
||||
.bind(("0.0.0.0", port))?
|
||||
.run()
|
Loading…
Reference in New Issue
Block a user