Compare commits
3 Commits
08d3682164
...
4d39e40648
Author | SHA1 | Date | |
---|---|---|---|
4d39e40648 | |||
f5258d42e4 | |||
ce961e8703 |
3
API/.gitignore
vendored
3
API/.gitignore
vendored
@ -1,3 +1,6 @@
|
|||||||
database.sqlite3*
|
database.sqlite3*
|
||||||
efbundle
|
efbundle
|
||||||
|
appsettings.Development.json
|
||||||
|
appsettings.Production.json
|
||||||
|
appsettings.json
|
||||||
|
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"JwtSettings": {
|
|
||||||
"Issuer": "Flutter-SkanTravels",
|
|
||||||
"Audience": "Mercantec-Elever",
|
|
||||||
"Key": "DenHerMåAldrigVæreOffentligKunIDetteDemoProjekt"
|
|
||||||
},
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Data Source=database.sqlite3"
|
|
||||||
},
|
|
||||||
"AccessKey": "783f66e09776e12d0f49fd5d6be0eedf",
|
|
||||||
"SecretKey": "4485259e3dd9f00fa6d9eedb3c5f55a9f256d4ea951d5a4fc0acd3e9d261da12"
|
|
||||||
}
|
|
@ -1,17 +1,18 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
enum ApiService {
|
enum ApiService {
|
||||||
auth,
|
auth,
|
||||||
app,
|
app,
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> request(BuildContext? context, ApiService service, String method, String path, Object? body) async {
|
Future<String?> request(BuildContext? context, ApiService service, String method, String path, dynamic body) async {
|
||||||
final messenger = context != null ? ScaffoldMessenger.of(context) : null;
|
final messenger = context != null ? ScaffoldMessenger.of(context) : null;
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ Future<String?> request(BuildContext? context, ApiService service, String method
|
|||||||
response = await function(
|
response = await function(
|
||||||
Uri.parse(host + path),
|
Uri.parse(host + path),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: body != null ? jsonEncode(body) : null,
|
body: body is Uint8List ? body : (body is Object ? jsonEncode(body) : null),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -103,7 +104,7 @@ Future<String?> putUser(
|
|||||||
'ProfilePicture', // field name matches your backend DTO
|
'ProfilePicture', // field name matches your backend DTO
|
||||||
fileStream,
|
fileStream,
|
||||||
length,
|
length,
|
||||||
filename: profilePicture.path.split('/').last,
|
filename: p.basename(profilePicture.path),
|
||||||
);
|
);
|
||||||
request.files.add(multipartFile);
|
request.files.add(multipartFile);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:mobile/base/sidemenu.dart';
|
import 'package:mobile/base/sidemenu.dart';
|
||||||
import 'models.dart';
|
import 'models.dart' as models;
|
||||||
import 'api.dart' as api;
|
import 'api.dart' as api;
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
class CreateReviewPage extends StatefulWidget {
|
class CreateReviewPage extends StatefulWidget {
|
||||||
const CreateReviewPage({super.key});
|
const CreateReviewPage({super.key});
|
||||||
@ -16,12 +18,13 @@ class CreateReviewPage extends StatefulWidget {
|
|||||||
class _CreateReviewState extends State<CreateReviewPage> {
|
class _CreateReviewState extends State<CreateReviewPage> {
|
||||||
final titleInput = TextEditingController();
|
final titleInput = TextEditingController();
|
||||||
final contentInput = TextEditingController();
|
final contentInput = TextEditingController();
|
||||||
Place? place;
|
models.Place? place;
|
||||||
var rating = 0;
|
var rating = 0;
|
||||||
|
File? _selectedImage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
place = ModalRoute.of(context)!.settings.arguments as Place;
|
place = ModalRoute.of(context)!.settings.arguments as models.Place;
|
||||||
|
|
||||||
return SideMenu(
|
return SideMenu(
|
||||||
selectedIndex: -1,
|
selectedIndex: -1,
|
||||||
@ -56,8 +59,16 @@ class _CreateReviewState extends State<CreateReviewPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
// Review Stars
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
IconButton(onPressed: _pickImageFromGallery, icon: const Icon(Icons.image, color: Colors.grey), tooltip: "Pick an image from your gallery",),
|
||||||
|
IconButton(onPressed: _pickImageFromCamera, icon: const Icon(Icons.camera_alt, color: Colors.grey), tooltip: "Take a picture with your camera",),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
// Review Stars
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -82,8 +93,35 @@ class _CreateReviewState extends State<CreateReviewPage> {
|
|||||||
titleInput.dispose();
|
titleInput.dispose();
|
||||||
contentInput.dispose();
|
contentInput.dispose();
|
||||||
}
|
}
|
||||||
|
Future _pickImageFromGallery() async {
|
||||||
|
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
|
||||||
|
if (image == null) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_selectedImage = File(image.path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _pickImageFromCamera() async {
|
||||||
|
final image = await ImagePicker().pickImage(source: ImageSource.camera);
|
||||||
|
if (image == null) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_selectedImage = File(image.path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _submitReview() async {
|
Future<void> _submitReview() async {
|
||||||
|
models.Image? image;
|
||||||
|
|
||||||
|
if (_selectedImage != null) {
|
||||||
|
final fileName = path.basename(_selectedImage!.path);
|
||||||
|
final response = await api.request(context, api.ApiService.app, 'POST', '/images?file_name=$fileName', _selectedImage!.readAsBytesSync());
|
||||||
|
if (response == null) return;
|
||||||
|
|
||||||
|
image = models.Image.fromJson(jsonDecode(response));
|
||||||
|
}
|
||||||
|
|
||||||
final response = await api.request(context, api.ApiService.app, 'POST', '/reviews', {
|
final response = await api.request(context, api.ApiService.app, 'POST', '/reviews', {
|
||||||
'title': titleInput.text,
|
'title': titleInput.text,
|
||||||
'content': contentInput.text,
|
'content': contentInput.text,
|
||||||
@ -92,11 +130,12 @@ class _CreateReviewState extends State<CreateReviewPage> {
|
|||||||
'rating': rating,
|
'rating': rating,
|
||||||
'lat': place!.point.latitude,
|
'lat': place!.point.latitude,
|
||||||
'lng': place!.point.longitude,
|
'lng': place!.point.longitude,
|
||||||
|
'image_id': image?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response == null || !mounted) return;
|
if (response == null || !mounted) return;
|
||||||
|
|
||||||
final review = Review.fromJson(jsonDecode(response));
|
final review = models.Review.fromJson(jsonDecode(response));
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Review submitted')));
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Review submitted')));
|
||||||
|
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:mobile/models.dart';
|
import 'package:mobile/models.dart' as models;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'api.dart' as api;
|
import 'api.dart' as api;
|
||||||
import 'base/variables.dart';
|
import 'base/variables.dart';
|
||||||
|
|
||||||
class EditProfilePage extends StatefulWidget {
|
class EditProfilePage extends StatefulWidget {
|
||||||
final User? userData;
|
final models.User? userData;
|
||||||
|
|
||||||
const EditProfilePage({super.key, required this.userData});
|
const EditProfilePage({super.key, required this.userData});
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ class _ProfilePageState extends State<EditProfilePage> {
|
|||||||
TextEditingController confirmPasswordInput = TextEditingController();
|
TextEditingController confirmPasswordInput = TextEditingController();
|
||||||
File? _selectedImage;
|
File? _selectedImage;
|
||||||
|
|
||||||
set userData(User userData) {}
|
set userData(models.User userData) {}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -48,16 +48,18 @@ class _ProfilePageState extends State<EditProfilePage> {
|
|||||||
Future _pickImageFromGallery() async{
|
Future _pickImageFromGallery() async{
|
||||||
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
|
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
setState(() {
|
|
||||||
_selectedImage = File(image.path);
|
setState(() {
|
||||||
|
_selectedImage = File(image.path);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _pickImageFromCamera() async{
|
Future _pickImageFromCamera() async{
|
||||||
final image = await ImagePicker().pickImage(source: ImageSource.camera);
|
final image = await ImagePicker().pickImage(source: ImageSource.camera);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
setState(() {
|
|
||||||
_selectedImage = File(image.path);
|
setState(() {
|
||||||
|
_selectedImage = File(image.path);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,9 +158,9 @@ class _ProfilePageState extends State<EditProfilePage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Edit Profile'),
|
title: const Text('Edit Profile'),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context); // Navigates back when the back button is pressed
|
Navigator.pop(context); // Navigates back when the back button is pressed
|
||||||
},
|
},
|
||||||
@ -170,71 +172,77 @@ class _ProfilePageState extends State<EditProfilePage> {
|
|||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
controller: usernameInput,
|
controller: usernameInput,
|
||||||
decoration: InputDecoration(labelText: 'Name'),
|
decoration: const InputDecoration(labelText: 'Name'),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
controller: emailInput,
|
controller: emailInput,
|
||||||
decoration: InputDecoration(labelText: 'Email'),
|
decoration: const InputDecoration(labelText: 'Email'),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
controller: passwordInput,
|
controller: passwordInput,
|
||||||
decoration: InputDecoration(labelText: 'New password'),
|
decoration: const InputDecoration(labelText: 'New password'),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
controller: confirmPasswordInput,
|
controller: confirmPasswordInput,
|
||||||
decoration: InputDecoration(labelText: 'Repeat new password'),
|
decoration: const InputDecoration(labelText: 'Repeat new password'),
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('ProfilePicture',
|
const Text('Profile Picture', style: TextStyle(fontSize: 17)),
|
||||||
style: TextStyle(fontSize: 17)),
|
if (_selectedImage != null)
|
||||||
if (_selectedImage != null)
|
ClipOval(
|
||||||
ClipOval(
|
child: Image(
|
||||||
child: Image(
|
image: FileImage(_selectedImage!),
|
||||||
image: FileImage(_selectedImage!),
|
height: 100,
|
||||||
height: 100,
|
width: 100,
|
||||||
width: 100,
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ClipOval(
|
|
||||||
child: Image(
|
|
||||||
image: NetworkImage(user!.profilePicture),
|
|
||||||
height: 100,
|
|
||||||
width: 100,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (_selectedImage != null)
|
)
|
||||||
Text(_selectedImage!.path.toString()),
|
else if (user!.profilePicture != null && user!.profilePicture.isNotEmpty)
|
||||||
//until here
|
ClipOval(
|
||||||
Row(
|
child: Image(
|
||||||
|
image: NetworkImage(user!.profilePicture),
|
||||||
|
height: 100,
|
||||||
|
width: 100,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Icon(
|
||||||
|
Icons.account_circle,
|
||||||
|
size: 100,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
if (_selectedImage != null)
|
||||||
|
Text(_selectedImage!.path.toString()),
|
||||||
|
//until here
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('Change using'),
|
const Text('Change using'),
|
||||||
TextButton(onPressed: _pickImageFromGallery, child: Text('Gallery')),
|
TextButton(onPressed: _pickImageFromGallery, child: const Text('Gallery')),
|
||||||
SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
Text('or'),
|
const Text('or'),
|
||||||
SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
TextButton(onPressed: _pickImageFromCamera, child: Text('Camera'))
|
TextButton(onPressed: _pickImageFromCamera, child: const Text('Camera'))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _saveProfile, // Save and pop
|
onPressed: _saveProfile, // Save and pop
|
||||||
child: Text('Save'),
|
child: const Text('Save'),
|
||||||
),
|
),
|
||||||
SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => _deleteProfile(context),
|
onPressed: () => _deleteProfile(context),
|
||||||
child: Text('Delete'),
|
child: const Text('Delete'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
foregroundColor: Colors.red, // Red text
|
foregroundColor: Colors.red, // Red text
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:mobile/base/sidemenu.dart';
|
import 'package:mobile/base/sidemenu.dart';
|
||||||
import 'package:mobile/models.dart';
|
import 'package:mobile/models.dart' as models;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
@ -28,7 +28,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
|
|
||||||
// Assuming token is a JSON string
|
// Assuming token is a JSON string
|
||||||
Map<String, dynamic> json = jsonDecode(response);
|
Map<String, dynamic> json = jsonDecode(response);
|
||||||
Login jsonUser = Login.fromJson(json);
|
models.Login jsonUser = models.Login.fromJson(json);
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
prefs.setString('token', jsonUser.token);
|
prefs.setString('token', jsonUser.token);
|
||||||
|
@ -103,6 +103,22 @@ class User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Image {
|
||||||
|
int id;
|
||||||
|
String userId;
|
||||||
|
String imageUrl;
|
||||||
|
|
||||||
|
Image(this.id, this.userId, this.imageUrl);
|
||||||
|
|
||||||
|
factory Image.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Image(
|
||||||
|
json['id'],
|
||||||
|
json['user_id'],
|
||||||
|
json['image_url'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SearchResults {
|
class SearchResults {
|
||||||
LatLng location;
|
LatLng location;
|
||||||
String name;
|
String name;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:mobile/base/variables.dart';
|
import 'package:mobile/base/variables.dart';
|
||||||
import 'package:mobile/models.dart';
|
import 'package:mobile/models.dart' as models;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'base/sidemenu.dart';
|
import 'base/sidemenu.dart';
|
||||||
import 'api.dart' as api;
|
import 'api.dart' as api;
|
||||||
@ -15,7 +15,7 @@ class ProfilePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ProfilePageState extends State<ProfilePage> {
|
class _ProfilePageState extends State<ProfilePage> {
|
||||||
User? userData;
|
models.User? userData;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -39,9 +39,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
if (response == null) return;
|
if (response == null) return;
|
||||||
|
|
||||||
Map<String, dynamic> json = jsonDecode(response);
|
Map<String, dynamic> json = jsonDecode(response);
|
||||||
User jsonUser = User.fromJson(json);
|
models.User jsonUser = models.User.fromJson(json);
|
||||||
|
|
||||||
print(jsonUser.profilePicture);
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
userData = jsonUser;
|
userData = jsonUser;
|
||||||
@ -73,15 +71,15 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
: Column(
|
: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
userData?.profilePicture != null ?
|
userData?.profilePicture != null && userData!.profilePicture.isNotEmpty ?
|
||||||
ClipOval(
|
ClipOval(
|
||||||
child: Image(
|
child: Image(
|
||||||
image: NetworkImage(userData!.profilePicture),
|
image: NetworkImage(userData!.profilePicture),
|
||||||
height: 100,
|
height: 100,
|
||||||
width: 100, // Ensure width matches the height to make it fully round
|
width: 100, // Ensure width matches the height to make it fully round
|
||||||
fit: BoxFit.cover, // This makes sure the image fits inside the circle properly
|
fit: BoxFit.cover, // This makes sure the image fits inside the circle properly
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Icon(
|
: const Icon(
|
||||||
Icons.account_circle,
|
Icons.account_circle,
|
||||||
size: 100,
|
size: 100,
|
||||||
|
@ -33,6 +33,7 @@ dependencies:
|
|||||||
http: ^1.2.1
|
http: ^1.2.1
|
||||||
flutter_map: ^7.0.2
|
flutter_map: ^7.0.2
|
||||||
latlong2: ^0.9.1
|
latlong2: ^0.9.1
|
||||||
|
path: ^1.9.0
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
JWT_SECRET=DenHerMåAldrigVæreOffentligKunIDetteDemoProjekt
|
JWT_SECRET=DenHerMåAldrigVæreOffentligKunIDetteDemoProjekt
|
||||||
AWS_ACCESS_KEY_ID=
|
AWS_ACCESS_KEY_ID=
|
||||||
AWS_SECRET_ACCESS_KEY=
|
AWS_SECRET_ACCESS_KEY=
|
||||||
AWS_SESSION_TOKEN=
|
AWS_ENDPOINT_URL=
|
||||||
AWS_REGION=
|
AWS_REGION=
|
||||||
R2_BUCKET_NAME=
|
R2_BUCKET_NAME=
|
||||||
R2_BUCKET_URL=
|
R2_BUCKET_URL=
|
||||||
|
98
rust-backend/Cargo.lock
generated
98
rust-backend/Cargo.lock
generated
@ -243,6 +243,55 @@ version = "0.2.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "0.6.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.81"
|
version = "0.1.81"
|
||||||
@ -779,6 +828,12 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
@ -973,6 +1028,29 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_filter"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_logger"
|
||||||
|
version = "0.11.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"env_filter",
|
||||||
|
"humantime",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -1276,6 +1354,12 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humantime"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.30"
|
version = "0.14.30"
|
||||||
@ -1361,6 +1445,12 @@ version = "2.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
@ -2172,9 +2262,11 @@ dependencies = [
|
|||||||
"actix-utils",
|
"actix-utils",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
|
"aws-credential-types",
|
||||||
"aws-sdk-s3",
|
"aws-sdk-s3",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
"env_logger",
|
||||||
"hmac",
|
"hmac",
|
||||||
"refinery",
|
"refinery",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@ -2539,6 +2631,12 @@ version = "2.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
env_logger = "0.11"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
@ -18,4 +19,4 @@ tokio = { version = "1", features = ["full"] }
|
|||||||
refinery = { version = "0.8.14", features = ["rusqlite"] }
|
refinery = { version = "0.8.14", features = ["rusqlite"] }
|
||||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
reqwest = { version = "0.11.16", features = ["blocking", "json"] }
|
reqwest = { version = "0.11.16", features = ["blocking", "json"] }
|
||||||
|
aws-credential-types = { version = "1.2.1", features = ["hardcoded-credentials"] }
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE reviews ADD COLUMN image_id INT;
|
@ -8,6 +8,10 @@ use models::{Favorite, Review, Image};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
use aws_sdk_s3::primitives::ByteStream;
|
use aws_sdk_s3::primitives::ByteStream;
|
||||||
|
use aws_sdk_s3::config::Region;
|
||||||
|
use rusqlite::types::Null;
|
||||||
|
use env_logger;
|
||||||
|
use aws_sdk_s3::config::Credentials;
|
||||||
|
|
||||||
mod embedded {
|
mod embedded {
|
||||||
use refinery::embed_migrations;
|
use refinery::embed_migrations;
|
||||||
@ -141,14 +145,40 @@ struct CreateReviewRequest {
|
|||||||
title: String,
|
title: String,
|
||||||
content: String,
|
content: String,
|
||||||
rating: i64,
|
rating: i64,
|
||||||
|
image_id: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/reviews")]
|
#[post("/reviews")]
|
||||||
async fn create_review(auth: AuthorizedUser, data: web::Data<AppData>, input: web::Json<CreateReviewRequest>) -> impl Responder {
|
async fn create_review(auth: AuthorizedUser, data: web::Data<AppData>, input: web::Json<CreateReviewRequest>) -> impl Responder {
|
||||||
let db = data.database.lock().unwrap();
|
let db = data.database.lock().unwrap();
|
||||||
|
|
||||||
|
let image_id = match input.image_id {
|
||||||
|
Some(image_id) => image_id.to_string(),
|
||||||
|
None => "NULL".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
match db.execute(
|
match db.execute(
|
||||||
"INSERT INTO reviews (user_id, lat, lng, place_name, place_description, title, content, rating) VALUES (:user_id, :lat, :lng, :place_name, :place_description, :title, :content, :rating)",
|
"INSERT INTO reviews (
|
||||||
|
user_id,
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
place_name,
|
||||||
|
place_description,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
rating,
|
||||||
|
image_id
|
||||||
|
) VALUES (
|
||||||
|
:user_id,
|
||||||
|
:lat,
|
||||||
|
:lng,
|
||||||
|
:place_name,
|
||||||
|
:place_description,
|
||||||
|
:title,
|
||||||
|
:content,
|
||||||
|
:rating,
|
||||||
|
:image_id
|
||||||
|
)",
|
||||||
&[
|
&[
|
||||||
(":user_id", &auth.user_id),
|
(":user_id", &auth.user_id),
|
||||||
(":lat", &input.lat.to_string()),
|
(":lat", &input.lat.to_string()),
|
||||||
@ -158,6 +188,7 @@ async fn create_review(auth: AuthorizedUser, data: web::Data<AppData>, input: we
|
|||||||
(":title", &input.title),
|
(":title", &input.title),
|
||||||
(":content", &input.content),
|
(":content", &input.content),
|
||||||
(":rating", &input.rating.to_string()),
|
(":rating", &input.rating.to_string()),
|
||||||
|
(":image_id", &image_id),
|
||||||
],
|
],
|
||||||
) {
|
) {
|
||||||
Ok(_) => HttpResponse::Created().json(Review {
|
Ok(_) => HttpResponse::Created().json(Review {
|
||||||
@ -209,7 +240,14 @@ struct CreateImageQuery {
|
|||||||
async fn create_image(auth: AuthorizedUser, data: web::Data<AppData>, bytes: Bytes, query: web::Query<CreateImageQuery>) -> impl Responder {
|
async fn create_image(auth: AuthorizedUser, data: web::Data<AppData>, bytes: Bytes, query: web::Query<CreateImageQuery>) -> impl Responder {
|
||||||
let db = data.database.lock().unwrap();
|
let db = data.database.lock().unwrap();
|
||||||
let config = aws_config::load_from_env().await;
|
let config = aws_config::load_from_env().await;
|
||||||
let client = aws_sdk_s3::Client::new(&config);
|
|
||||||
|
println!("{:?}", config);
|
||||||
|
|
||||||
|
let s3_config = aws_sdk_s3::config::Builder::from(&config)
|
||||||
|
.force_path_style(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let client = aws_sdk_s3::Client::from_conf(s3_config);
|
||||||
|
|
||||||
let bucket_name = std::env::var("R2_BUCKET_NAME").expect("R2_BUCKET_NAME must be provided");
|
let bucket_name = std::env::var("R2_BUCKET_NAME").expect("R2_BUCKET_NAME must be provided");
|
||||||
let bucket_url = std::env::var("R2_BUCKET_URL").expect("R2_BUCKET_URL must be provided");
|
let bucket_url = std::env::var("R2_BUCKET_URL").expect("R2_BUCKET_URL must be provided");
|
||||||
@ -222,13 +260,14 @@ async fn create_image(auth: AuthorizedUser, data: web::Data<AppData>, bytes: Byt
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
if response.is_err() {
|
if response.is_err() {
|
||||||
|
println!("{:?}", response.unwrap_err());
|
||||||
return HttpResponse::InternalServerError().finish();
|
return HttpResponse::InternalServerError().finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
let image_url = format!("{}/{}/{}", bucket_url, bucket_name, query.file_name);
|
let image_url = format!("{}/{}/{}", bucket_url, bucket_name, query.file_name);
|
||||||
|
|
||||||
match db.execute(
|
match db.execute(
|
||||||
"INSERT INTO images (user_id, image_url)",
|
"INSERT INTO images (user_id, image_url) VALUES (:user_id, :image_url)",
|
||||||
&[
|
&[
|
||||||
(":user_id", &auth.user_id),
|
(":user_id", &auth.user_id),
|
||||||
(":image_url", &image_url),
|
(":image_url", &image_url),
|
||||||
@ -245,6 +284,8 @@ async fn create_image(auth: AuthorizedUser, data: web::Data<AppData>, bytes: Byt
|
|||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
let _ = dotenvy::dotenv();
|
let _ = dotenvy::dotenv();
|
||||||
|
|
||||||
let port = std::env::var("RUST_BACKEND_PORT")
|
let port = std::env::var("RUST_BACKEND_PORT")
|
||||||
@ -278,6 +319,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.service(reviews)
|
.service(reviews)
|
||||||
.service(create_review)
|
.service(create_review)
|
||||||
.service(delete_review)
|
.service(delete_review)
|
||||||
|
.service(create_image)
|
||||||
})
|
})
|
||||||
.bind(("0.0.0.0", port))?
|
.bind(("0.0.0.0", port))?
|
||||||
.run()
|
.run()
|
||||||
|
Loading…
Reference in New Issue
Block a user