Compare commits

...

18 Commits

Author SHA1 Message Date
46f9535b4a Rename API to AuthorizationService 2024-08-15 14:03:27 +00:00
acb15df909 Rename services accordingly 2024-08-15 14:00:09 +00:00
7c2fccb79e Move to services/ 2024-08-15 14:00:06 +00:00
46bb1ce74c
Merge branch 'sidemenu_final' 2024-08-21 14:24:53 +02:00
206483de09 Update Docs/SCRUM Logbog.md 2024-08-21 12:55:16 +01:00
102e2d6ca2
Add log for this week 2024-08-21 13:51:25 +02:00
c4fc11c169 Update Docs/SCRUM Logbog.md 2024-08-21 12:34:42 +01:00
LilleBRG
6ee61650d1 menu done 2024-08-21 13:28:48 +02:00
8e42e54c98
Add logout button 2024-08-21 12:49:38 +02:00
eef47f6ee4
Implement registering 2024-08-21 10:19:02 +02:00
debbddd05b
Implement login and save token 2024-08-21 10:03:03 +02:00
c9769fcada
Add environment variables 2024-08-21 09:19:04 +02:00
LilleBRG
a082441826 menu partly done. 2024-08-20 16:46:13 +02:00
1c6fc39b14
Add register page 2024-08-20 14:53:06 +02:00
3bd2ba5cbc
Add login page 2024-08-20 14:04:40 +02:00
d2bfcc4692
Add map to flutter application 2024-08-20 12:38:50 +02:00
LilleBRG
f1327571e6 Updated UserModel, UpdatePassword made 2024-08-16 12:53:39 +02:00
bb44d2bc56
Add routes for creating and deleting favorites 2024-08-16 12:26:26 +02:00
67 changed files with 1173 additions and 373 deletions

View File

@ -1,6 +0,0 @@
namespace API.Application.Users.Commands
{
public class UpdateUserPassword
{
}
}

View File

@ -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
View File

@ -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

View 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
View 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
View 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`

View File

@ -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}"

View File

@ -0,0 +1,4 @@
{
"AUTH_SERVICE_HOST": "http://localhost:5287",
"APP_SERVICE_HOST": "http://localhost:8080"
}

81
Mobile/lib/api.dart Normal file
View 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;
}

View 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
View 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();
}
}

View File

@ -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
View 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
View 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();
}
}

View File

@ -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'),
),
],
),
),
);
}
}

View File

@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}

View File

@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@ -4,5 +4,7 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@ -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"

View File

@ -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:

View File

@ -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.

View File

@ -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)
{

View File

@ -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!
};
}
}

View File

@ -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
{

View File

@ -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
{

View File

@ -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
{

View File

@ -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);
}
}
}

View File

@ -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
{

View File

@ -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,

View File

@ -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'">

View File

@ -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

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
namespace AuthorizationService.Controllers
{
[ApiController]
[Route("[controller]")]

View File

@ -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)

View File

@ -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"]

View File

@ -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()

View File

@ -2,7 +2,7 @@
#nullable disable
namespace API.Migrations
namespace AuthorizationService.Migrations
{
/// <inheritdoc />
public partial class CreateUser : Migration

View File

@ -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()

View File

@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
namespace AuthorizationService.Migrations
{
/// <inheritdoc />
public partial class ChangedUserWithGuid : Migration

View File

@ -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");

View File

@ -2,7 +2,7 @@
#nullable disable
namespace API.Migrations
namespace AuthorizationService.Migrations
{
/// <inheritdoc />
public partial class NoMoreGuid : Migration

View 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
}
}
}

View File

@ -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: "");
}
}
}

View File

@ -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");

View File

@ -1,4 +1,4 @@
namespace API.Models
namespace AuthorizationService.Models
{
public class BaseModel
{

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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>();

View File

@ -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]

View File

@ -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()